diff options
Diffstat (limited to 'cts/lab/CTStests.py')
-rw-r--r-- | cts/lab/CTStests.py | 3178 |
1 files changed, 3178 insertions, 0 deletions
diff --git a/cts/lab/CTStests.py b/cts/lab/CTStests.py new file mode 100644 index 0000000..61766ce --- /dev/null +++ b/cts/lab/CTStests.py @@ -0,0 +1,3178 @@ +""" Test-specific classes for Pacemaker's Cluster Test Suite (CTS) +""" + +__copyright__ = "Copyright 2000-2023 the Pacemaker project contributors" +__license__ = "GNU General Public License version 2 or later (GPLv2+) WITHOUT ANY WARRANTY" + +# +# SPECIAL NOTE: +# +# Tests may NOT implement any cluster-manager-specific code in them. +# EXTEND the ClusterManager object to provide the base capabilities +# the test needs if you need to do something that the current CM classes +# do not. Otherwise you screw up the whole point of the object structure +# in CTS. +# +# Thank you. +# + +import os +import re +import time +import subprocess +import tempfile + +from stat import * +from cts.CTSaudits import * + +from pacemaker import BuildOptions +from pacemaker._cts.CTS import NodeStatus +from pacemaker._cts.environment import EnvFactory +from pacemaker._cts.logging import LogFactory +from pacemaker._cts.patterns import PatternSelector +from pacemaker._cts.remote import RemoteFactory +from pacemaker._cts.watcher import LogWatcher + +AllTestClasses = [ ] + + +class CTSTest(object): + ''' + A Cluster test. + We implement the basic set of properties and behaviors for a generic + cluster test. + + Cluster tests track their own statistics. + We keep each of the kinds of counts we track as separate {name,value} + pairs. + ''' + + def __init__(self, cm): + #self.name="the unnamed test" + self.Stats = {"calls":0 + , "success":0 + , "failure":0 + , "skipped":0 + , "auditfail":0} + +# if not issubclass(cm.__class__, ClusterManager): +# raise ValueError("Must be a ClusterManager object") + self.CM = cm + self.Env = EnvFactory().getInstance() + self.rsh = RemoteFactory().getInstance() + self.logger = LogFactory() + self.templates = PatternSelector(cm["Name"]) + self.Audits = [] + self.timeout = 120 + self.passed = 1 + self.is_loop = 0 + self.is_unsafe = 0 + self.is_experimental = 0 + self.is_container = 0 + self.is_valgrind = 0 + self.benchmark = 0 # which tests to benchmark + self.timer = {} # timers + + def log(self, args): + self.logger.log(args) + + def debug(self, args): + self.logger.debug(args) + + def has_key(self, key): + return key in self.Stats + + def __setitem__(self, key, value): + self.Stats[key] = value + + def __getitem__(self, key): + if str(key) == "0": + raise ValueError("Bad call to 'foo in X', should reference 'foo in X.Stats' instead") + + if key in self.Stats: + return self.Stats[key] + return None + + def log_mark(self, msg): + self.debug("MARK: test %s %s %d" % (self.name,msg,time.time())) + return + + def get_timer(self,key = "test"): + try: return self.timer[key] + except: return 0 + + def set_timer(self,key = "test"): + self.timer[key] = time.time() + return self.timer[key] + + def log_timer(self,key = "test"): + elapsed = 0 + if key in self.timer: + elapsed = time.time() - self.timer[key] + s = key == "test" and self.name or "%s:%s" % (self.name,key) + self.debug("%s runtime: %.2f" % (s, elapsed)) + del self.timer[key] + return elapsed + + def incr(self, name): + '''Increment (or initialize) the value associated with the given name''' + if not name in self.Stats: + self.Stats[name] = 0 + self.Stats[name] = self.Stats[name]+1 + + # Reset the test passed boolean + if name == "calls": + self.passed = 1 + + def failure(self, reason="none"): + '''Increment the failure count''' + self.passed = 0 + self.incr("failure") + self.logger.log(("Test %s" % self.name).ljust(35) + " FAILED: %s" % reason) + return None + + def success(self): + '''Increment the success count''' + self.incr("success") + return 1 + + def skipped(self): + '''Increment the skipped count''' + self.incr("skipped") + return 1 + + def __call__(self, node): + '''Perform the given test''' + raise ValueError("Abstract Class member (__call__)") + self.incr("calls") + return self.failure() + + def audit(self): + passed = 1 + if len(self.Audits) > 0: + for audit in self.Audits: + if not audit(): + self.logger.log("Internal %s Audit %s FAILED." % (self.name, audit.name())) + self.incr("auditfail") + passed = 0 + return passed + + def setup(self, node): + '''Setup the given test''' + return self.success() + + def teardown(self, node): + '''Tear down the given test''' + return self.success() + + def create_watch(self, patterns, timeout, name=None): + if not name: + name = self.name + return LogWatcher(self.Env["LogFileName"], patterns, self.Env["nodes"], self.Env["LogWatcher"], name, timeout) + + def local_badnews(self, prefix, watch, local_ignore=[]): + errcount = 0 + if not prefix: + prefix = "LocalBadNews:" + + ignorelist = [] + ignorelist.append(" CTS: ") + ignorelist.append(prefix) + ignorelist.extend(local_ignore) + + while errcount < 100: + match = watch.look(0) + if match: + add_err = 1 + for ignore in ignorelist: + if add_err == 1 and re.search(ignore, match): + add_err = 0 + if add_err == 1: + self.logger.log(prefix + " " + match) + errcount = errcount + 1 + else: + break + else: + self.logger.log("Too many errors!") + + watch.end() + return errcount + + def is_applicable(self): + return self.is_applicable_common() + + def is_applicable_common(self): + '''Return True if we are applicable in the current test configuration''' + #raise ValueError("Abstract Class member (is_applicable)") + + if self.is_loop and not self.Env["loop-tests"]: + return False + elif self.is_unsafe and not self.Env["unsafe-tests"]: + return False + elif self.is_valgrind and not self.Env["valgrind-tests"]: + return False + elif self.is_experimental and not self.Env["experimental-tests"]: + return False + elif self.is_container and not self.Env["container-tests"]: + return False + elif self.Env["benchmark"] and self.benchmark == 0: + return False + + return True + + def find_ocfs2_resources(self, node): + self.r_o2cb = None + self.r_ocfs2 = [] + + (_, lines) = self.rsh(node, "crm_resource -c", verbose=1) + for line in lines: + if re.search("^Resource", line): + r = AuditResource(self.CM, line) + if r.rtype == "o2cb" and r.parent != "NA": + self.debug("Found o2cb: %s" % self.r_o2cb) + self.r_o2cb = r.parent + if re.search("^Constraint", line): + c = AuditConstraint(self.CM, line) + if c.type == "rsc_colocation" and c.target == self.r_o2cb: + self.r_ocfs2.append(c.rsc) + + self.debug("Found ocfs2 filesystems: %s" % repr(self.r_ocfs2)) + return len(self.r_ocfs2) + + def canrunnow(self, node): + '''Return TRUE if we can meaningfully run right now''' + return 1 + + def errorstoignore(self): + '''Return list of errors which are 'normal' and should be ignored''' + return [] + + +class StopTest(CTSTest): + '''Stop (deactivate) the cluster manager on a node''' + def __init__(self, cm): + CTSTest.__init__(self, cm) + self.name = "Stop" + + def __call__(self, node): + '''Perform the 'stop' test. ''' + self.incr("calls") + if self.CM.ShouldBeStatus[node] != "up": + return self.skipped() + + patterns = [] + # Technically we should always be able to notice ourselves stopping + patterns.append(self.templates["Pat:We_stopped"] % node) + + # Any active node needs to notice this one left + # (note that this won't work if we have multiple partitions) + for other in self.Env["nodes"]: + if self.CM.ShouldBeStatus[other] == "up" and other != node: + patterns.append(self.templates["Pat:They_stopped"] %(other, self.CM.key_for_node(node))) + #self.debug("Checking %s will notice %s left"%(other, node)) + + watch = self.create_watch(patterns, self.Env["DeadTime"]) + watch.set_watch() + + if node == self.CM.OurNode: + self.incr("us") + else: + if self.CM.upcount() <= 1: + self.incr("all") + else: + self.incr("them") + + self.CM.StopaCM(node) + watch_result = watch.look_for_all() + + failreason = None + UnmatchedList = "||" + if watch.unmatched: + (_, output) = self.rsh(node, "/bin/ps axf", verbose=1) + for line in output: + self.debug(line) + + (_, output) = self.rsh(node, "/usr/sbin/dlm_tool dump 2>/dev/null", verbose=1) + for line in output: + self.debug(line) + + for regex in watch.unmatched: + self.logger.log ("ERROR: Shutdown pattern not found: %s" % (regex)) + UnmatchedList += regex + "||"; + failreason = "Missing shutdown pattern" + + self.CM.cluster_stable(self.Env["DeadTime"]) + + if not watch.unmatched or self.CM.upcount() == 0: + return self.success() + + if len(watch.unmatched) >= self.CM.upcount(): + return self.failure("no match against (%s)" % UnmatchedList) + + if failreason == None: + return self.success() + else: + return self.failure(failreason) +# +# We don't register StopTest because it's better when called by +# another test... +# + + +class StartTest(CTSTest): + '''Start (activate) the cluster manager on a node''' + def __init__(self, cm, debug=None): + CTSTest.__init__(self,cm) + self.name = "start" + self.debug = debug + + def __call__(self, node): + '''Perform the 'start' test. ''' + self.incr("calls") + + if self.CM.upcount() == 0: + self.incr("us") + else: + self.incr("them") + + if self.CM.ShouldBeStatus[node] != "down": + return self.skipped() + elif self.CM.StartaCM(node): + return self.success() + else: + return self.failure("Startup %s on node %s failed" + % (self.Env["Name"], node)) + +# +# We don't register StartTest because it's better when called by +# another test... +# + + +class FlipTest(CTSTest): + '''If it's running, stop it. If it's stopped start it. + Overthrow the status quo... + ''' + def __init__(self, cm): + CTSTest.__init__(self,cm) + self.name = "Flip" + self.start = StartTest(cm) + self.stop = StopTest(cm) + + def __call__(self, node): + '''Perform the 'Flip' test. ''' + self.incr("calls") + if self.CM.ShouldBeStatus[node] == "up": + self.incr("stopped") + ret = self.stop(node) + type = "up->down" + # Give the cluster time to recognize it's gone... + time.sleep(self.Env["StableTime"]) + elif self.CM.ShouldBeStatus[node] == "down": + self.incr("started") + ret = self.start(node) + type = "down->up" + else: + return self.skipped() + + self.incr(type) + if ret: + return self.success() + else: + return self.failure("%s failure" % type) + +# Register FlipTest as a good test to run +AllTestClasses.append(FlipTest) + + +class RestartTest(CTSTest): + '''Stop and restart a node''' + def __init__(self, cm): + CTSTest.__init__(self,cm) + self.name = "Restart" + self.start = StartTest(cm) + self.stop = StopTest(cm) + self.benchmark = 1 + + def __call__(self, node): + '''Perform the 'restart' test. ''' + self.incr("calls") + + self.incr("node:" + node) + + ret1 = 1 + if self.CM.StataCM(node): + self.incr("WasStopped") + if not self.start(node): + return self.failure("start (setup) failure: "+node) + + self.set_timer() + if not self.stop(node): + return self.failure("stop failure: "+node) + if not self.start(node): + return self.failure("start failure: "+node) + return self.success() + +# Register RestartTest as a good test to run +AllTestClasses.append(RestartTest) + + +class StonithdTest(CTSTest): + def __init__(self, cm): + CTSTest.__init__(self, cm) + self.name = "Stonithd" + self.startall = SimulStartLite(cm) + self.benchmark = 1 + + def __call__(self, node): + self.incr("calls") + if len(self.Env["nodes"]) < 2: + return self.skipped() + + ret = self.startall(None) + if not ret: + return self.failure("Setup failed") + + is_dc = self.CM.is_node_dc(node) + + watchpats = [] + watchpats.append(self.templates["Pat:Fencing_ok"] % node) + watchpats.append(self.templates["Pat:NodeFenced"] % node) + + if not self.Env["at-boot"]: + self.debug("Expecting %s to stay down" % node) + self.CM.ShouldBeStatus[node] = "down" + else: + self.debug("Expecting %s to come up again %d" % (node, self.Env["at-boot"])) + watchpats.append("%s.* S_STARTING -> S_PENDING" % node) + watchpats.append("%s.* S_PENDING -> S_NOT_DC" % node) + + watch = self.create_watch(watchpats, 30 + self.Env["DeadTime"] + self.Env["StableTime"] + self.Env["StartTime"]) + watch.set_watch() + + origin = self.Env.random_gen.choice(self.Env["nodes"]) + + (rc, _) = self.rsh(origin, "stonith_admin --reboot %s -VVVVVV" % node) + + if rc == 124: # CRM_EX_TIMEOUT + # Look for the patterns, usually this means the required + # device was running on the node to be fenced - or that + # the required devices were in the process of being loaded + # and/or moved + # + # Effectively the node committed suicide so there will be + # no confirmation, but pacemaker should be watching and + # fence the node again + + self.logger.log("Fencing command on %s to fence %s timed out" % (origin, node)) + + elif origin != node and rc != 0: + self.debug("Waiting for the cluster to recover") + self.CM.cluster_stable() + + self.debug("Waiting for fenced node to come back up") + self.CM.ns.wait_for_all_nodes(self.Env["nodes"], 600) + + self.logger.log("Fencing command on %s failed to fence %s (rc=%d)" % (origin, node, rc)) + + elif origin == node and rc != 255: + # 255 == broken pipe, ie. the node was fenced as expected + self.logger.log("Locally originated fencing returned %d" % rc) + + self.set_timer("fence") + matched = watch.look_for_all() + self.log_timer("fence") + self.set_timer("reform") + if watch.unmatched: + self.logger.log("Patterns not found: " + repr(watch.unmatched)) + + self.debug("Waiting for the cluster to recover") + self.CM.cluster_stable() + + self.debug("Waiting for fenced node to come back up") + self.CM.ns.wait_for_all_nodes(self.Env["nodes"], 600) + + self.debug("Waiting for the cluster to re-stabilize with all nodes") + is_stable = self.CM.cluster_stable(self.Env["StartTime"]) + + if not matched: + return self.failure("Didn't find all expected patterns") + elif not is_stable: + return self.failure("Cluster did not become stable") + + self.log_timer("reform") + return self.success() + + def errorstoignore(self): + return [ + self.templates["Pat:Fencing_start"] % ".*", + self.templates["Pat:Fencing_ok"] % ".*", + self.templates["Pat:Fencing_active"], + r"error.*: Operation 'reboot' targeting .* by .* for stonith_admin.*: Timer expired", + ] + + def is_applicable(self): + if not self.is_applicable_common(): + return False + + if "DoFencing" in list(self.Env.keys()): + return self.Env["DoFencing"] + + return True + +AllTestClasses.append(StonithdTest) + + +class StartOnebyOne(CTSTest): + '''Start all the nodes ~ one by one''' + def __init__(self, cm): + CTSTest.__init__(self,cm) + self.name = "StartOnebyOne" + self.stopall = SimulStopLite(cm) + self.start = StartTest(cm) + self.ns = NodeStatus(cm.Env) + + def __call__(self, dummy): + '''Perform the 'StartOnebyOne' test. ''' + self.incr("calls") + + # We ignore the "node" parameter... + + # Shut down all the nodes... + ret = self.stopall(None) + if not ret: + return self.failure("Test setup failed") + + failed = [] + self.set_timer() + for node in self.Env["nodes"]: + if not self.start(node): + failed.append(node) + + if len(failed) > 0: + return self.failure("Some node failed to start: " + repr(failed)) + + return self.success() + +# Register StartOnebyOne as a good test to run +AllTestClasses.append(StartOnebyOne) + + +class SimulStart(CTSTest): + '''Start all the nodes ~ simultaneously''' + def __init__(self, cm): + CTSTest.__init__(self,cm) + self.name = "SimulStart" + self.stopall = SimulStopLite(cm) + self.startall = SimulStartLite(cm) + + def __call__(self, dummy): + '''Perform the 'SimulStart' test. ''' + self.incr("calls") + + # We ignore the "node" parameter... + + # Shut down all the nodes... + ret = self.stopall(None) + if not ret: + return self.failure("Setup failed") + + if not self.startall(None): + return self.failure("Startall failed") + + return self.success() + +# Register SimulStart as a good test to run +AllTestClasses.append(SimulStart) + + +class SimulStop(CTSTest): + '''Stop all the nodes ~ simultaneously''' + def __init__(self, cm): + CTSTest.__init__(self,cm) + self.name = "SimulStop" + self.startall = SimulStartLite(cm) + self.stopall = SimulStopLite(cm) + + def __call__(self, dummy): + '''Perform the 'SimulStop' test. ''' + self.incr("calls") + + # We ignore the "node" parameter... + + # Start up all the nodes... + ret = self.startall(None) + if not ret: + return self.failure("Setup failed") + + if not self.stopall(None): + return self.failure("Stopall failed") + + return self.success() + +# Register SimulStop as a good test to run +AllTestClasses.append(SimulStop) + + +class StopOnebyOne(CTSTest): + '''Stop all the nodes in order''' + def __init__(self, cm): + CTSTest.__init__(self,cm) + self.name = "StopOnebyOne" + self.startall = SimulStartLite(cm) + self.stop = StopTest(cm) + + def __call__(self, dummy): + '''Perform the 'StopOnebyOne' test. ''' + self.incr("calls") + + # We ignore the "node" parameter... + + # Start up all the nodes... + ret = self.startall(None) + if not ret: + return self.failure("Setup failed") + + failed = [] + self.set_timer() + for node in self.Env["nodes"]: + if not self.stop(node): + failed.append(node) + + if len(failed) > 0: + return self.failure("Some node failed to stop: " + repr(failed)) + + return self.success() + +# Register StopOnebyOne as a good test to run +AllTestClasses.append(StopOnebyOne) + + +class RestartOnebyOne(CTSTest): + '''Restart all the nodes in order''' + def __init__(self, cm): + CTSTest.__init__(self,cm) + self.name = "RestartOnebyOne" + self.startall = SimulStartLite(cm) + + def __call__(self, dummy): + '''Perform the 'RestartOnebyOne' test. ''' + self.incr("calls") + + # We ignore the "node" parameter... + + # Start up all the nodes... + ret = self.startall(None) + if not ret: + return self.failure("Setup failed") + + did_fail = [] + self.set_timer() + self.restart = RestartTest(self.CM) + for node in self.Env["nodes"]: + if not self.restart(node): + did_fail.append(node) + + if did_fail: + return self.failure("Could not restart %d nodes: %s" + % (len(did_fail), repr(did_fail))) + return self.success() + +# Register StopOnebyOne as a good test to run +AllTestClasses.append(RestartOnebyOne) + + +class PartialStart(CTSTest): + '''Start a node - but tell it to stop before it finishes starting up''' + def __init__(self, cm): + CTSTest.__init__(self,cm) + self.name = "PartialStart" + self.startall = SimulStartLite(cm) + self.stopall = SimulStopLite(cm) + self.stop = StopTest(cm) + #self.is_unsafe = 1 + + def __call__(self, node): + '''Perform the 'PartialStart' test. ''' + self.incr("calls") + + ret = self.stopall(None) + if not ret: + return self.failure("Setup failed") + + watchpats = [] + watchpats.append("pacemaker-controld.*Connecting to .* cluster infrastructure") + watch = self.create_watch(watchpats, self.Env["DeadTime"]+10) + watch.set_watch() + + self.CM.StartaCMnoBlock(node) + ret = watch.look_for_all() + if not ret: + self.logger.log("Patterns not found: " + repr(watch.unmatched)) + return self.failure("Setup of %s failed" % node) + + ret = self.stop(node) + if not ret: + return self.failure("%s did not stop in time" % node) + + return self.success() + + def errorstoignore(self): + '''Return list of errors which should be ignored''' + + # We might do some fencing in the 2-node case if we make it up far enough + return [ + r"Executing reboot fencing operation", + r"Requesting fencing \([^)]+\) targeting node ", + ] + +# Register StopOnebyOne as a good test to run +AllTestClasses.append(PartialStart) + + +class StandbyTest(CTSTest): + def __init__(self, cm): + CTSTest.__init__(self,cm) + self.name = "Standby" + self.benchmark = 1 + + self.start = StartTest(cm) + self.startall = SimulStartLite(cm) + + # make sure the node is active + # set the node to standby mode + # check resources, none resource should be running on the node + # set the node to active mode + # check resouces, resources should have been migrated back (SHOULD THEY?) + + def __call__(self, node): + + self.incr("calls") + ret = self.startall(None) + if not ret: + return self.failure("Start all nodes failed") + + self.debug("Make sure node %s is active" % node) + if self.CM.StandbyStatus(node) != "off": + if not self.CM.SetStandbyMode(node, "off"): + return self.failure("can't set node %s to active mode" % node) + + self.CM.cluster_stable() + + status = self.CM.StandbyStatus(node) + if status != "off": + return self.failure("standby status of %s is [%s] but we expect [off]" % (node, status)) + + self.debug("Getting resources running on node %s" % node) + rsc_on_node = self.CM.active_resources(node) + + watchpats = [] + watchpats.append(r"State transition .* -> S_POLICY_ENGINE") + watch = self.create_watch(watchpats, self.Env["DeadTime"]+10) + watch.set_watch() + + self.debug("Setting node %s to standby mode" % node) + if not self.CM.SetStandbyMode(node, "on"): + return self.failure("can't set node %s to standby mode" % node) + + self.set_timer("on") + + ret = watch.look_for_all() + if not ret: + self.logger.log("Patterns not found: " + repr(watch.unmatched)) + self.CM.SetStandbyMode(node, "off") + return self.failure("cluster didn't react to standby change on %s" % node) + + self.CM.cluster_stable() + + status = self.CM.StandbyStatus(node) + if status != "on": + return self.failure("standby status of %s is [%s] but we expect [on]" % (node, status)) + self.log_timer("on") + + self.debug("Checking resources") + bad_run = self.CM.active_resources(node) + if len(bad_run) > 0: + rc = self.failure("%s set to standby, %s is still running on it" % (node, repr(bad_run))) + self.debug("Setting node %s to active mode" % node) + self.CM.SetStandbyMode(node, "off") + return rc + + self.debug("Setting node %s to active mode" % node) + if not self.CM.SetStandbyMode(node, "off"): + return self.failure("can't set node %s to active mode" % node) + + self.set_timer("off") + self.CM.cluster_stable() + + status = self.CM.StandbyStatus(node) + if status != "off": + return self.failure("standby status of %s is [%s] but we expect [off]" % (node, status)) + self.log_timer("off") + + return self.success() + +AllTestClasses.append(StandbyTest) + + +class ValgrindTest(CTSTest): + '''Check for memory leaks''' + def __init__(self, cm): + CTSTest.__init__(self,cm) + self.name = "Valgrind" + self.stopall = SimulStopLite(cm) + self.startall = SimulStartLite(cm) + self.is_valgrind = 1 + self.is_loop = 1 + + def setup(self, node): + self.incr("calls") + + ret = self.stopall(None) + if not ret: + return self.failure("Stop all nodes failed") + + # @TODO Edit /etc/sysconfig/pacemaker on all nodes to enable valgrind, + # and clear any valgrind logs from previous runs. For now, we rely on + # the user to do this manually. + + ret = self.startall(None) + if not ret: + return self.failure("Start all nodes failed") + + return self.success() + + def teardown(self, node): + # Return all nodes to normal + # @TODO Edit /etc/sysconfig/pacemaker on all nodes to disable valgrind + ret = self.stopall(None) + if not ret: + return self.failure("Stop all nodes failed") + + return self.success() + + def find_leaks(self): + # Check for leaks + # (no longer used but kept in case feature is restored) + leaked = [] + self.stop = StopTest(self.CM) + + for node in self.Env["nodes"]: + rc = self.stop(node) + if not rc: + self.failure("Couldn't shut down %s" % node) + + (rc, _) = self.rsh(node, "grep -e indirectly.*lost:.*[1-9] -e definitely.*lost:.*[1-9] -e (ERROR|error).*SUMMARY:.*[1-9].*errors %s" % self.logger.logPat) + if rc != 1: + leaked.append(node) + self.failure("Valgrind errors detected on %s" % node) + (_, output) = self.rsh(node, "grep -e lost: -e SUMMARY: %s" % self.logger.logPat, verbose=1) + for line in output: + self.logger.log(line) + (_, output) = self.rsh(node, "cat %s" % self.logger.logPat, verbose=1) + for line in output: + self.debug(line) + + self.rsh(node, "rm -f %s" % self.logger.logPat, verbose=1) + return leaked + + def __call__(self, node): + #leaked = self.find_leaks() + #if len(leaked) > 0: + # return self.failure("Nodes %s leaked" % repr(leaked)) + + return self.success() + + def errorstoignore(self): + '''Return list of errors which should be ignored''' + return [ + r"pacemaker-based.*: \*\*\*\*\*\*\*\*\*\*\*\*\*", + r"pacemaker-based.*: .* avoid confusing Valgrind", + r"HA_VALGRIND_ENABLED", + ] + + +class StandbyLoopTest(ValgrindTest): + '''Check for memory leaks by putting a node in and out of standby for an hour''' + # @TODO This is not a useful test for memory leaks + def __init__(self, cm): + ValgrindTest.__init__(self,cm) + self.name = "StandbyLoop" + + def __call__(self, node): + + lpc = 0 + delay = 2 + failed = 0 + done = time.time() + self.Env["loop-minutes"] * 60 + while time.time() <= done and not failed: + lpc = lpc + 1 + + time.sleep(delay) + if not self.CM.SetStandbyMode(node, "on"): + self.failure("can't set node %s to standby mode" % node) + failed = lpc + + time.sleep(delay) + if not self.CM.SetStandbyMode(node, "off"): + self.failure("can't set node %s to active mode" % node) + failed = lpc + + leaked = self.find_leaks() + if failed: + return self.failure("Iteration %d failed" % failed) + elif len(leaked) > 0: + return self.failure("Nodes %s leaked" % repr(leaked)) + + return self.success() + +#AllTestClasses.append(StandbyLoopTest) + + +class BandwidthTest(CTSTest): +# Tests should not be cluster-manager-specific +# If you need to find out cluster manager configuration to do this, then +# it should be added to the generic cluster manager API. + '''Test the bandwidth which the cluster uses''' + def __init__(self, cm): + CTSTest.__init__(self, cm) + self.name = "Bandwidth" + self.start = StartTest(cm) + self.__setitem__("min",0) + self.__setitem__("max",0) + self.__setitem__("totalbandwidth",0) + (handle, self.tempfile) = tempfile.mkstemp(".cts") + os.close(handle) + self.startall = SimulStartLite(cm) + + def __call__(self, node): + '''Perform the Bandwidth test''' + self.incr("calls") + + if self.CM.upcount() < 1: + return self.skipped() + + Path = self.CM.InternalCommConfig() + if "ip" not in Path["mediatype"]: + return self.skipped() + + port = Path["port"][0] + port = int(port) + + ret = self.startall(None) + if not ret: + return self.failure("Test setup failed") + time.sleep(5) # We get extra messages right after startup. + + fstmpfile = "/var/run/band_estimate" + dumpcmd = "tcpdump -p -n -c 102 -i any udp port %d > %s 2>&1" \ + % (port, fstmpfile) + + (rc, _) = self.rsh(node, dumpcmd) + if rc == 0: + farfile = "root@%s:%s" % (node, fstmpfile) + self.rsh.copy(farfile, self.tempfile) + Bandwidth = self.countbandwidth(self.tempfile) + if not Bandwidth: + self.logger.log("Could not compute bandwidth.") + return self.success() + intband = int(Bandwidth + 0.5) + self.logger.log("...bandwidth: %d bits/sec" % intband) + self.Stats["totalbandwidth"] = self.Stats["totalbandwidth"] + Bandwidth + if self.Stats["min"] == 0: + self.Stats["min"] = Bandwidth + if Bandwidth > self.Stats["max"]: + self.Stats["max"] = Bandwidth + if Bandwidth < self.Stats["min"]: + self.Stats["min"] = Bandwidth + self.rsh(node, "rm -f %s" % fstmpfile) + os.unlink(self.tempfile) + return self.success() + else: + return self.failure("no response from tcpdump command [%d]!" % rc) + + def countbandwidth(self, file): + fp = open(file, "r") + fp.seek(0) + count = 0 + sum = 0 + while 1: + line = fp.readline() + if not line: + return None + if re.search("udp",line) or re.search("UDP,", line): + count = count + 1 + linesplit = line.split(" ") + for j in range(len(linesplit)-1): + if linesplit[j] == "udp": break + if linesplit[j] == "length:": break + + try: + sum = sum + int(linesplit[j+1]) + except ValueError: + self.logger.log("Invalid tcpdump line: %s" % line) + return None + T1 = linesplit[0] + timesplit = T1.split(":") + time2split = timesplit[2].split(".") + time1 = (int(timesplit[0])*60+int(timesplit[1]))*60+int(time2split[0])+int(time2split[1])*0.000001 + break + + while count < 100: + line = fp.readline() + if not line: + return None + if re.search("udp",line) or re.search("UDP,", line): + count = count+1 + linessplit = line.split(" ") + for j in range(len(linessplit)-1): + if linessplit[j] == "udp": break + if linessplit[j] == "length:": break + try: + sum = int(linessplit[j+1]) + sum + except ValueError: + self.logger.log("Invalid tcpdump line: %s" % line) + return None + + T2 = linessplit[0] + timesplit = T2.split(":") + time2split = timesplit[2].split(".") + time2 = (int(timesplit[0])*60+int(timesplit[1]))*60+int(time2split[0])+int(time2split[1])*0.000001 + time = time2-time1 + if (time <= 0): + return 0 + return int((sum*8)/time) + + def is_applicable(self): + '''BandwidthTest never applicable''' + return False + +AllTestClasses.append(BandwidthTest) + + +################################################################### +class MaintenanceMode(CTSTest): +################################################################### + def __init__(self, cm): + CTSTest.__init__(self,cm) + self.name = "MaintenanceMode" + self.start = StartTest(cm) + self.startall = SimulStartLite(cm) + self.max = 30 + #self.is_unsafe = 1 + self.benchmark = 1 + self.action = "asyncmon" + self.interval = 0 + self.rid = "maintenanceDummy" + + def toggleMaintenanceMode(self, node, action): + pats = [] + pats.append(self.templates["Pat:DC_IDLE"]) + + # fail the resource right after turning Maintenance mode on + # verify it is not recovered until maintenance mode is turned off + if action == "On": + pats.append(self.templates["Pat:RscOpFail"] % (self.action, self.rid)) + else: + pats.append(self.templates["Pat:RscOpOK"] % ("stop", self.rid)) + pats.append(self.templates["Pat:RscOpOK"] % ("start", self.rid)) + + watch = self.create_watch(pats, 60) + watch.set_watch() + + self.debug("Turning maintenance mode %s" % action) + self.rsh(node, self.templates["MaintenanceMode%s" % (action)]) + if (action == "On"): + self.rsh(node, "crm_resource -V -F -r %s -H %s &>/dev/null" % (self.rid, node)) + + self.set_timer("recover%s" % (action)) + watch.look_for_all() + self.log_timer("recover%s" % (action)) + if watch.unmatched: + self.debug("Failed to find patterns when turning maintenance mode %s" % action) + return repr(watch.unmatched) + + return "" + + def insertMaintenanceDummy(self, node): + pats = [] + pats.append(("%s.*" % node) + (self.templates["Pat:RscOpOK"] % ("start", self.rid))) + + watch = self.create_watch(pats, 60) + watch.set_watch() + + self.CM.AddDummyRsc(node, self.rid) + + self.set_timer("addDummy") + watch.look_for_all() + self.log_timer("addDummy") + + if watch.unmatched: + self.debug("Failed to find patterns when adding maintenance dummy resource") + return repr(watch.unmatched) + return "" + + def removeMaintenanceDummy(self, node): + pats = [] + pats.append(self.templates["Pat:RscOpOK"] % ("stop", self.rid)) + + watch = self.create_watch(pats, 60) + watch.set_watch() + self.CM.RemoveDummyRsc(node, self.rid) + + self.set_timer("removeDummy") + watch.look_for_all() + self.log_timer("removeDummy") + + if watch.unmatched: + self.debug("Failed to find patterns when removing maintenance dummy resource") + return repr(watch.unmatched) + return "" + + def managedRscList(self, node): + rscList = [] + (_, lines) = self.rsh(node, "crm_resource -c", verbose=1) + for line in lines: + if re.search("^Resource", line): + tmp = AuditResource(self.CM, line) + if tmp.managed(): + rscList.append(tmp.id) + + return rscList + + def verifyResources(self, node, rscList, managed): + managedList = list(rscList) + managed_str = "managed" + if not managed: + managed_str = "unmanaged" + + (_, lines) = self.rsh(node, "crm_resource -c", verbose=1) + for line in lines: + if re.search("^Resource", line): + tmp = AuditResource(self.CM, line) + if managed and not tmp.managed(): + continue + elif not managed and tmp.managed(): + continue + elif managedList.count(tmp.id): + managedList.remove(tmp.id) + + if len(managedList) == 0: + self.debug("Found all %s resources on %s" % (managed_str, node)) + return True + + self.logger.log("Could not find all %s resources on %s. %s" % (managed_str, node, managedList)) + return False + + def __call__(self, node): + '''Perform the 'MaintenanceMode' test. ''' + self.incr("calls") + verify_managed = False + verify_unmanaged = False + failPat = "" + + ret = self.startall(None) + if not ret: + return self.failure("Setup failed") + + # get a list of all the managed resources. We use this list + # after enabling maintenance mode to verify all managed resources + # become un-managed. After maintenance mode is turned off, we use + # this list to verify all the resources become managed again. + managedResources = self.managedRscList(node) + if len(managedResources) == 0: + self.logger.log("No managed resources on %s" % node) + return self.skipped() + + # insert a fake resource we can fail during maintenance mode + # so we can verify recovery does not take place until after maintenance + # mode is disabled. + failPat = failPat + self.insertMaintenanceDummy(node) + + # toggle maintenance mode ON, then fail dummy resource. + failPat = failPat + self.toggleMaintenanceMode(node, "On") + + # verify all the resources are now unmanaged + if self.verifyResources(node, managedResources, False): + verify_unmanaged = True + + # Toggle maintenance mode OFF, verify dummy is recovered. + failPat = failPat + self.toggleMaintenanceMode(node, "Off") + + # verify all the resources are now managed again + if self.verifyResources(node, managedResources, True): + verify_managed = True + + # Remove our maintenance dummy resource. + failPat = failPat + self.removeMaintenanceDummy(node) + + self.CM.cluster_stable() + + if failPat != "": + return self.failure("Unmatched patterns: %s" % (failPat)) + elif verify_unmanaged is False: + return self.failure("Failed to verify resources became unmanaged during maintenance mode") + elif verify_managed is False: + return self.failure("Failed to verify resources switched back to managed after disabling maintenance mode") + + return self.success() + + def errorstoignore(self): + '''Return list of errors which should be ignored''' + return [ + r"Updating failcount for %s" % self.rid, + r"schedulerd.*: Recover\s+%s\s+\(.*\)" % self.rid, + r"Unknown operation: fail", + self.templates["Pat:RscOpOK"] % (self.action, self.rid), + r"(ERROR|error).*: Action %s_%s_%d .* initiated outside of a transition" % (self.rid, self.action, self.interval), + ] + +AllTestClasses.append(MaintenanceMode) + + +class ResourceRecover(CTSTest): + def __init__(self, cm): + CTSTest.__init__(self,cm) + self.name = "ResourceRecover" + self.start = StartTest(cm) + self.startall = SimulStartLite(cm) + self.max = 30 + self.rid = None + self.rid_alt = None + #self.is_unsafe = 1 + self.benchmark = 1 + + # these are the values used for the new LRM API call + self.action = "asyncmon" + self.interval = 0 + + def __call__(self, node): + '''Perform the 'ResourceRecover' test. ''' + self.incr("calls") + + ret = self.startall(None) + if not ret: + return self.failure("Setup failed") + + # List all resources active on the node (skip test if none) + resourcelist = self.CM.active_resources(node) + if len(resourcelist) == 0: + self.logger.log("No active resources on %s" % node) + return self.skipped() + + # Choose one resource at random + rsc = self.choose_resource(node, resourcelist) + if rsc is None: + return self.failure("Could not get details of resource '%s'" % self.rid) + if rsc.id == rsc.clone_id: + self.debug("Failing " + rsc.id) + else: + self.debug("Failing " + rsc.id + " (also known as " + rsc.clone_id + ")") + + # Log patterns to watch for (failure, plus restart if managed) + pats = [] + pats.append(self.templates["Pat:CloneOpFail"] % (self.action, rsc.id, rsc.clone_id)) + if rsc.managed(): + pats.append(self.templates["Pat:RscOpOK"] % ("stop", self.rid)) + if rsc.unique(): + pats.append(self.templates["Pat:RscOpOK"] % ("start", self.rid)) + else: + # Anonymous clones may get restarted with a different clone number + pats.append(self.templates["Pat:RscOpOK"] % ("start", ".*")) + + # Fail resource. (Ideally, we'd fail it twice, to ensure the fail count + # is incrementing properly, but it might restart on a different node. + # We'd have to temporarily ban it from all other nodes and ensure the + # migration-threshold hasn't been reached.) + if self.fail_resource(rsc, node, pats) is None: + return None # self.failure() already called + + return self.success() + + def choose_resource(self, node, resourcelist): + """ Choose a random resource to target """ + + self.rid = self.Env.random_gen.choice(resourcelist) + self.rid_alt = self.rid + (_, lines) = self.rsh(node, "crm_resource -c", verbose=1) + for line in lines: + if line.startswith("Resource: "): + rsc = AuditResource(self.CM, line) + if rsc.id == self.rid: + # Handle anonymous clones that get renamed + self.rid = rsc.clone_id + return rsc + return None + + def get_failcount(self, node): + """ Check the fail count of targeted resource on given node """ + + (rc, lines) = self.rsh(node, + "crm_failcount --quiet --query --resource %s " + "--operation %s --interval %d " + "--node %s" % (self.rid, self.action, + self.interval, node), verbose=1) + if rc != 0 or len(lines) != 1: + self.logger.log("crm_failcount on %s failed (%d): %s" % (node, rc, + " // ".join(map(str.strip, lines)))) + return -1 + try: + failcount = int(lines[0]) + except (IndexError, ValueError): + self.logger.log("crm_failcount output on %s unparseable: %s" % (node, + ' '.join(lines))) + return -1 + return failcount + + def fail_resource(self, rsc, node, pats): + """ Fail the targeted resource, and verify as expected """ + + orig_failcount = self.get_failcount(node) + + watch = self.create_watch(pats, 60) + watch.set_watch() + + self.rsh(node, "crm_resource -V -F -r %s -H %s &>/dev/null" % (self.rid, node)) + + self.set_timer("recover") + watch.look_for_all() + self.log_timer("recover") + + self.CM.cluster_stable() + recovered = self.CM.ResourceLocation(self.rid) + + if watch.unmatched: + return self.failure("Patterns not found: %s" % repr(watch.unmatched)) + + elif rsc.unique() and len(recovered) > 1: + return self.failure("%s is now active on more than one node: %s"%(self.rid, repr(recovered))) + + elif len(recovered) > 0: + self.debug("%s is running on: %s" % (self.rid, repr(recovered))) + + elif rsc.managed(): + return self.failure("%s was not recovered and is inactive" % self.rid) + + new_failcount = self.get_failcount(node) + if new_failcount != (orig_failcount + 1): + return self.failure("%s fail count is %d not %d" % (self.rid, + new_failcount, orig_failcount + 1)) + + return 0 # Anything but None is success + + def errorstoignore(self): + '''Return list of errors which should be ignored''' + return [ + r"Updating failcount for %s" % self.rid, + r"schedulerd.*: Recover\s+(%s|%s)\s+\(.*\)" % (self.rid, self.rid_alt), + r"Unknown operation: fail", + self.templates["Pat:RscOpOK"] % (self.action, self.rid), + r"(ERROR|error).*: Action %s_%s_%d .* initiated outside of a transition" % (self.rid, self.action, self.interval), + ] + +AllTestClasses.append(ResourceRecover) + + +class ComponentFail(CTSTest): + def __init__(self, cm): + CTSTest.__init__(self,cm) + self.name = "ComponentFail" + self.startall = SimulStartLite(cm) + self.complist = cm.Components() + self.patterns = [] + self.okerrpatterns = [] + self.is_unsafe = 1 + + def __call__(self, node): + '''Perform the 'ComponentFail' test. ''' + self.incr("calls") + self.patterns = [] + self.okerrpatterns = [] + + # start all nodes + ret = self.startall(None) + if not ret: + return self.failure("Setup failed") + + if not self.CM.cluster_stable(self.Env["StableTime"]): + return self.failure("Setup failed - unstable") + + node_is_dc = self.CM.is_node_dc(node, None) + + # select a component to kill + chosen = self.Env.random_gen.choice(self.complist) + while chosen.dc_only and node_is_dc == 0: + chosen = self.Env.random_gen.choice(self.complist) + + self.debug("...component %s (dc=%d)" % (chosen.name, node_is_dc)) + self.incr(chosen.name) + + if chosen.name != "corosync": + self.patterns.append(self.templates["Pat:ChildKilled"] %(node, chosen.name)) + self.patterns.append(self.templates["Pat:ChildRespawn"] %(node, chosen.name)) + + self.patterns.extend(chosen.pats) + if node_is_dc: + self.patterns.extend(chosen.dc_pats) + + # @TODO this should be a flag in the Component + if chosen.name in [ "corosync", "pacemaker-based", "pacemaker-fenced" ]: + # Ignore actions for fence devices if fencer will respawn + # (their registration will be lost, and probes will fail) + self.okerrpatterns = [ self.templates["Pat:Fencing_active"] ] + (_, lines) = self.rsh(node, "crm_resource -c", verbose=1) + for line in lines: + if re.search("^Resource", line): + r = AuditResource(self.CM, line) + if r.rclass == "stonith": + self.okerrpatterns.append(self.templates["Pat:Fencing_recover"] % r.id) + self.okerrpatterns.append(self.templates["Pat:Fencing_probe"] % r.id) + + # supply a copy so self.patterns doesn't end up empty + tmpPats = [] + tmpPats.extend(self.patterns) + self.patterns.extend(chosen.badnews_ignore) + + # Look for STONITH ops, depending on Env["at-boot"] we might need to change the nodes status + stonithPats = [] + stonithPats.append(self.templates["Pat:Fencing_ok"] % node) + stonith = self.create_watch(stonithPats, 0) + stonith.set_watch() + + # set the watch for stable + watch = self.create_watch( + tmpPats, self.Env["DeadTime"] + self.Env["StableTime"] + self.Env["StartTime"]) + watch.set_watch() + + # kill the component + chosen.kill(node) + + self.debug("Waiting for the cluster to recover") + self.CM.cluster_stable() + + self.debug("Waiting for any fenced node to come back up") + self.CM.ns.wait_for_all_nodes(self.Env["nodes"], 600) + + self.debug("Waiting for the cluster to re-stabilize with all nodes") + self.CM.cluster_stable(self.Env["StartTime"]) + + self.debug("Checking if %s was shot" % node) + shot = stonith.look(60) + if shot: + self.debug("Found: " + repr(shot)) + self.okerrpatterns.append(self.templates["Pat:Fencing_start"] % node) + + if not self.Env["at-boot"]: + self.CM.ShouldBeStatus[node] = "down" + + # If fencing occurred, chances are many (if not all) the expected logs + # will not be sent - or will be lost when the node reboots + return self.success() + + # check for logs indicating a graceful recovery + matched = watch.look_for_all(allow_multiple_matches=True) + if watch.unmatched: + self.logger.log("Patterns not found: " + repr(watch.unmatched)) + + self.debug("Waiting for the cluster to re-stabilize with all nodes") + is_stable = self.CM.cluster_stable(self.Env["StartTime"]) + + if not matched: + return self.failure("Didn't find all expected %s patterns" % chosen.name) + elif not is_stable: + return self.failure("Cluster did not become stable after killing %s" % chosen.name) + + return self.success() + + def errorstoignore(self): + '''Return list of errors which should be ignored''' + # Note that okerrpatterns refers to the last time we ran this test + # The good news is that this works fine for us... + self.okerrpatterns.extend(self.patterns) + return self.okerrpatterns + +AllTestClasses.append(ComponentFail) + + +class SplitBrainTest(CTSTest): + '''It is used to test split-brain. when the path between the two nodes break + check the two nodes both take over the resource''' + def __init__(self,cm): + CTSTest.__init__(self,cm) + self.name = "SplitBrain" + self.start = StartTest(cm) + self.startall = SimulStartLite(cm) + self.is_experimental = 1 + + def isolate_partition(self, partition): + other_nodes = [] + other_nodes.extend(self.Env["nodes"]) + + for node in partition: + try: + other_nodes.remove(node) + except ValueError: + self.logger.log("Node "+node+" not in " + repr(self.Env["nodes"]) + " from " +repr(partition)) + + if len(other_nodes) == 0: + return 1 + + self.debug("Creating partition: " + repr(partition)) + self.debug("Everyone else: " + repr(other_nodes)) + + for node in partition: + if not self.CM.isolate_node(node, other_nodes): + self.logger.log("Could not isolate %s" % node) + return 0 + + return 1 + + def heal_partition(self, partition): + other_nodes = [] + other_nodes.extend(self.Env["nodes"]) + + for node in partition: + try: + other_nodes.remove(node) + except ValueError: + self.logger.log("Node "+node+" not in " + repr(self.Env["nodes"])) + + if len(other_nodes) == 0: + return 1 + + self.debug("Healing partition: " + repr(partition)) + self.debug("Everyone else: " + repr(other_nodes)) + + for node in partition: + self.CM.unisolate_node(node, other_nodes) + + def __call__(self, node): + '''Perform split-brain test''' + self.incr("calls") + self.passed = 1 + partitions = {} + + ret = self.startall(None) + if not ret: + return self.failure("Setup failed") + + while 1: + # Retry until we get multiple partitions + partitions = {} + p_max = len(self.Env["nodes"]) + for node in self.Env["nodes"]: + p = self.Env.random_gen.randint(1, p_max) + if not p in partitions: + partitions[p] = [] + partitions[p].append(node) + p_max = len(list(partitions.keys())) + if p_max > 1: + break + # else, try again + + self.debug("Created %d partitions" % p_max) + for key in list(partitions.keys()): + self.debug("Partition["+str(key)+"]:\t"+repr(partitions[key])) + + # Disabling STONITH to reduce test complexity for now + self.rsh(node, "crm_attribute -V -n stonith-enabled -v false") + + for key in list(partitions.keys()): + self.isolate_partition(partitions[key]) + + count = 30 + while count > 0: + if len(self.CM.find_partitions()) != p_max: + time.sleep(10) + else: + break + else: + self.failure("Expected partitions were not created") + + # Target number of partitions formed - wait for stability + if not self.CM.cluster_stable(): + self.failure("Partitioned cluster not stable") + + # Now audit the cluster state + self.CM.partitions_expected = p_max + if not self.audit(): + self.failure("Audits failed") + self.CM.partitions_expected = 1 + + # And heal them again + for key in list(partitions.keys()): + self.heal_partition(partitions[key]) + + # Wait for a single partition to form + count = 30 + while count > 0: + if len(self.CM.find_partitions()) != 1: + time.sleep(10) + count -= 1 + else: + break + else: + self.failure("Cluster did not reform") + + # Wait for it to have the right number of members + count = 30 + while count > 0: + members = [] + + partitions = self.CM.find_partitions() + if len(partitions) > 0: + members = partitions[0].split() + + if len(members) != len(self.Env["nodes"]): + time.sleep(10) + count -= 1 + else: + break + else: + self.failure("Cluster did not completely reform") + + # Wait up to 20 minutes - the delay is more preferable than + # trying to continue with in a messed up state + if not self.CM.cluster_stable(1200): + self.failure("Reformed cluster not stable") + if self.Env["continue"]: + answer = "Y" + else: + try: + answer = input('Continue? [nY]') + except EOFError as e: + answer = "n" + if answer and answer == "n": + raise ValueError("Reformed cluster not stable") + + # Turn fencing back on + if self.Env["DoFencing"]: + self.rsh(node, "crm_attribute -V -D -n stonith-enabled") + + self.CM.cluster_stable() + + if self.passed: + return self.success() + return self.failure("See previous errors") + + def errorstoignore(self): + '''Return list of errors which are 'normal' and should be ignored''' + return [ + r"Another DC detected:", + r"(ERROR|error).*: .*Application of an update diff failed", + r"pacemaker-controld.*:.*not in our membership list", + r"CRIT:.*node.*returning after partition", + ] + + def is_applicable(self): + if not self.is_applicable_common(): + return False + return len(self.Env["nodes"]) > 2 + +AllTestClasses.append(SplitBrainTest) + + +class Reattach(CTSTest): + def __init__(self, cm): + CTSTest.__init__(self,cm) + self.name = "Reattach" + self.startall = SimulStartLite(cm) + self.restart1 = RestartTest(cm) + self.stopall = SimulStopLite(cm) + self.is_unsafe = 0 # Handled by canrunnow() + + def _is_managed(self, node): + (_, is_managed) = self.rsh(node, "crm_attribute -t rsc_defaults -n is-managed -q -G -d true", verbose=1) + is_managed = is_managed[0].strip() + return is_managed == "true" + + def _set_unmanaged(self, node): + self.debug("Disable resource management") + self.rsh(node, "crm_attribute -t rsc_defaults -n is-managed -v false") + + def _set_managed(self, node): + self.debug("Re-enable resource management") + self.rsh(node, "crm_attribute -t rsc_defaults -n is-managed -D") + + def setup(self, node): + attempt = 0 + if not self.startall(None): + return None + + # Make sure we are really _really_ stable and that all + # resources, including those that depend on transient node + # attributes, are started + while not self.CM.cluster_stable(double_check=True): + if attempt < 5: + attempt += 1 + self.debug("Not stable yet, re-testing") + else: + self.logger.log("Cluster is not stable") + return None + + return 1 + + def teardown(self, node): + + # Make sure 'node' is up + start = StartTest(self.CM) + start(node) + + if not self._is_managed(node): + self.logger.log("Attempting to re-enable resource management on %s" % node) + self._set_managed(node) + self.CM.cluster_stable() + if not self._is_managed(node): + self.logger.log("Could not re-enable resource management") + return 0 + + return 1 + + def canrunnow(self, node): + '''Return TRUE if we can meaningfully run right now''' + if self.find_ocfs2_resources(node): + self.logger.log("Detach/Reattach scenarios are not possible with OCFS2 services present") + return 0 + return 1 + + def __call__(self, node): + self.incr("calls") + + pats = [] + # Conveniently, the scheduler will display this message when disabling + # management, even if fencing is not enabled, so we can rely on it. + managed = self.create_watch(["No fencing will be done"], 60) + managed.set_watch() + + self._set_unmanaged(node) + + if not managed.look_for_all(): + self.logger.log("Patterns not found: " + repr(managed.unmatched)) + return self.failure("Resource management not disabled") + + pats = [] + pats.append(self.templates["Pat:RscOpOK"] % ("start", ".*")) + pats.append(self.templates["Pat:RscOpOK"] % ("stop", ".*")) + pats.append(self.templates["Pat:RscOpOK"] % ("promote", ".*")) + pats.append(self.templates["Pat:RscOpOK"] % ("demote", ".*")) + pats.append(self.templates["Pat:RscOpOK"] % ("migrate", ".*")) + + watch = self.create_watch(pats, 60, "ShutdownActivity") + watch.set_watch() + + self.debug("Shutting down the cluster") + ret = self.stopall(None) + if not ret: + self._set_managed(node) + return self.failure("Couldn't shut down the cluster") + + self.debug("Bringing the cluster back up") + ret = self.startall(None) + time.sleep(5) # allow ping to update the CIB + if not ret: + self._set_managed(node) + return self.failure("Couldn't restart the cluster") + + if self.local_badnews("ResourceActivity:", watch): + self._set_managed(node) + return self.failure("Resources stopped or started during cluster restart") + + watch = self.create_watch(pats, 60, "StartupActivity") + watch.set_watch() + + # Re-enable resource management (and verify it happened). + self._set_managed(node) + self.CM.cluster_stable() + if not self._is_managed(node): + return self.failure("Could not re-enable resource management") + + # Ignore actions for STONITH resources + ignore = [] + (_, lines) = self.rsh(node, "crm_resource -c", verbose=1) + for line in lines: + if re.search("^Resource", line): + r = AuditResource(self.CM, line) + if r.rclass == "stonith": + + self.debug("Ignoring start actions for %s" % r.id) + ignore.append(self.templates["Pat:RscOpOK"] % ("start", r.id)) + + if self.local_badnews("ResourceActivity:", watch, ignore): + return self.failure("Resources stopped or started after resource management was re-enabled") + + return ret + + def errorstoignore(self): + '''Return list of errors which should be ignored''' + return [ + r"resource( was|s were) active at shutdown", + ] + + def is_applicable(self): + return True + +AllTestClasses.append(Reattach) + + +class SpecialTest1(CTSTest): + '''Set up a custom test to cause quorum failure issues for Andrew''' + def __init__(self, cm): + CTSTest.__init__(self,cm) + self.name = "SpecialTest1" + self.startall = SimulStartLite(cm) + self.restart1 = RestartTest(cm) + self.stopall = SimulStopLite(cm) + + def __call__(self, node): + '''Perform the 'SpecialTest1' test for Andrew. ''' + self.incr("calls") + + # Shut down all the nodes... + ret = self.stopall(None) + if not ret: + return self.failure("Could not stop all nodes") + + # Test config recovery when the other nodes come up + self.rsh(node, "rm -f " + BuildOptions.CIB_DIR + "/cib*") + + # Start the selected node + ret = self.restart1(node) + if not ret: + return self.failure("Could not start "+node) + + # Start all remaining nodes + ret = self.startall(None) + if not ret: + return self.failure("Could not start the remaining nodes") + + return self.success() + + def errorstoignore(self): + '''Return list of errors which should be ignored''' + # Errors that occur as a result of the CIB being wiped + return [ + r"error.*: v1 patchset error, patch failed to apply: Application of an update diff failed", + r"error.*: Resource start-up disabled since no STONITH resources have been defined", + r"error.*: Either configure some or disable STONITH with the stonith-enabled option", + r"error.*: NOTE: Clusters with shared data need STONITH to ensure data integrity", + ] + +AllTestClasses.append(SpecialTest1) + + +class HAETest(CTSTest): + '''Set up a custom test to cause quorum failure issues for Andrew''' + def __init__(self, cm): + CTSTest.__init__(self,cm) + self.name = "HAETest" + self.stopall = SimulStopLite(cm) + self.startall = SimulStartLite(cm) + self.is_loop = 1 + + def setup(self, node): + # Start all remaining nodes + ret = self.startall(None) + if not ret: + return self.failure("Couldn't start all nodes") + return self.success() + + def teardown(self, node): + # Stop everything + ret = self.stopall(None) + if not ret: + return self.failure("Couldn't stop all nodes") + return self.success() + + def wait_on_state(self, node, resource, expected_clones, attempts=240): + while attempts > 0: + active = 0 + (rc, lines) = self.rsh(node, "crm_resource -r %s -W -Q" % resource, verbose=1) + + # Hack until crm_resource does the right thing + if rc == 0 and lines: + active = len(lines) + + if len(lines) == expected_clones: + return 1 + + elif rc == 1: + self.debug("Resource %s is still inactive" % resource) + + elif rc == 234: + self.logger.log("Unknown resource %s" % resource) + return 0 + + elif rc == 246: + self.logger.log("Cluster is inactive") + return 0 + + elif rc != 0: + self.logger.log("Call to crm_resource failed, rc=%d" % rc) + return 0 + + else: + self.debug("Resource %s is active on %d times instead of %d" % (resource, active, expected_clones)) + + attempts -= 1 + time.sleep(1) + + return 0 + + def find_dlm(self, node): + self.r_dlm = None + + (_, lines) = self.rsh(node, "crm_resource -c", verbose=1) + for line in lines: + if re.search("^Resource", line): + r = AuditResource(self.CM, line) + if r.rtype == "controld" and r.parent != "NA": + self.debug("Found dlm: %s" % self.r_dlm) + self.r_dlm = r.parent + return 1 + return 0 + + def find_hae_resources(self, node): + self.r_dlm = None + self.r_o2cb = None + self.r_ocfs2 = [] + + if self.find_dlm(node): + self.find_ocfs2_resources(node) + + def is_applicable(self): + if not self.is_applicable_common(): + return False + if self.Env["Schema"] == "hae": + return True + return None + + +class HAERoleTest(HAETest): + def __init__(self, cm): + '''Lars' mount/unmount test for the HA extension. ''' + HAETest.__init__(self,cm) + self.name = "HAERoleTest" + + def change_state(self, node, resource, target): + (rc, _) = self.rsh(node, "crm_resource -V -r %s -p target-role -v %s --meta" % (resource, target)) + return rc + + def __call__(self, node): + self.incr("calls") + lpc = 0 + failed = 0 + delay = 2 + done = time.time() + self.Env["loop-minutes"]*60 + self.find_hae_resources(node) + + clone_max = len(self.Env["nodes"]) + while time.time() <= done and not failed: + lpc = lpc + 1 + + self.change_state(node, self.r_dlm, "Stopped") + if not self.wait_on_state(node, self.r_dlm, 0): + self.failure("%s did not go down correctly" % self.r_dlm) + failed = lpc + + self.change_state(node, self.r_dlm, "Started") + if not self.wait_on_state(node, self.r_dlm, clone_max): + self.failure("%s did not come up correctly" % self.r_dlm) + failed = lpc + + if not self.wait_on_state(node, self.r_o2cb, clone_max): + self.failure("%s did not come up correctly" % self.r_o2cb) + failed = lpc + + for fs in self.r_ocfs2: + if not self.wait_on_state(node, fs, clone_max): + self.failure("%s did not come up correctly" % fs) + failed = lpc + + if failed: + return self.failure("iteration %d failed" % failed) + return self.success() + +AllTestClasses.append(HAERoleTest) + + +class HAEStandbyTest(HAETest): + '''Set up a custom test to cause quorum failure issues for Andrew''' + def __init__(self, cm): + HAETest.__init__(self,cm) + self.name = "HAEStandbyTest" + + def change_state(self, node, resource, target): + (rc, _) = self.rsh(node, "crm_standby -V -l reboot -v %s" % (target)) + return rc + + def __call__(self, node): + self.incr("calls") + + lpc = 0 + failed = 0 + done = time.time() + self.Env["loop-minutes"]*60 + self.find_hae_resources(node) + + clone_max = len(self.Env["nodes"]) + while time.time() <= done and not failed: + lpc = lpc + 1 + + self.change_state(node, self.r_dlm, "true") + if not self.wait_on_state(node, self.r_dlm, clone_max-1): + self.failure("%s did not go down correctly" % self.r_dlm) + failed = lpc + + self.change_state(node, self.r_dlm, "false") + if not self.wait_on_state(node, self.r_dlm, clone_max): + self.failure("%s did not come up correctly" % self.r_dlm) + failed = lpc + + if not self.wait_on_state(node, self.r_o2cb, clone_max): + self.failure("%s did not come up correctly" % self.r_o2cb) + failed = lpc + + for fs in self.r_ocfs2: + if not self.wait_on_state(node, fs, clone_max): + self.failure("%s did not come up correctly" % fs) + failed = lpc + + if failed: + return self.failure("iteration %d failed" % failed) + return self.success() + +AllTestClasses.append(HAEStandbyTest) + + +class NearQuorumPointTest(CTSTest): + ''' + This test brings larger clusters near the quorum point (50%). + In addition, it will test doing starts and stops at the same time. + + Here is how I think it should work: + - loop over the nodes and decide randomly which will be up and which + will be down Use a 50% probability for each of up/down. + - figure out what to do to get into that state from the current state + - in parallel, bring up those going up and bring those going down. + ''' + + def __init__(self, cm): + CTSTest.__init__(self,cm) + self.name = "NearQuorumPoint" + + def __call__(self, dummy): + '''Perform the 'NearQuorumPoint' test. ''' + self.incr("calls") + startset = [] + stopset = [] + + stonith = self.CM.prepare_fencing_watcher("NearQuorumPoint") + #decide what to do with each node + for node in self.Env["nodes"]: + action = self.Env.random_gen.choice(["start","stop"]) + #action = self.Env.random_gen.choice(["start","stop","no change"]) + if action == "start" : + startset.append(node) + elif action == "stop" : + stopset.append(node) + + self.debug("start nodes:" + repr(startset)) + self.debug("stop nodes:" + repr(stopset)) + + #add search patterns + watchpats = [ ] + for node in stopset: + if self.CM.ShouldBeStatus[node] == "up": + watchpats.append(self.templates["Pat:We_stopped"] % node) + + for node in startset: + if self.CM.ShouldBeStatus[node] == "down": + #watchpats.append(self.templates["Pat:NonDC_started"] % node) + watchpats.append(self.templates["Pat:Local_started"] % node) + else: + for stopping in stopset: + if self.CM.ShouldBeStatus[stopping] == "up": + watchpats.append(self.templates["Pat:They_stopped"] % (node, self.CM.key_for_node(stopping))) + + if len(watchpats) == 0: + return self.skipped() + + if len(startset) != 0: + watchpats.append(self.templates["Pat:DC_IDLE"]) + + watch = self.create_watch(watchpats, self.Env["DeadTime"]+10) + + watch.set_watch() + + #begin actions + for node in stopset: + if self.CM.ShouldBeStatus[node] == "up": + self.CM.StopaCMnoBlock(node) + + for node in startset: + if self.CM.ShouldBeStatus[node] == "down": + self.CM.StartaCMnoBlock(node) + + #get the result + if watch.look_for_all(): + self.CM.cluster_stable() + self.CM.fencing_cleanup("NearQuorumPoint", stonith) + return self.success() + + self.logger.log("Warn: Patterns not found: " + repr(watch.unmatched)) + + #get the "bad" nodes + upnodes = [] + for node in stopset: + if self.CM.StataCM(node) == 1: + upnodes.append(node) + + downnodes = [] + for node in startset: + if self.CM.StataCM(node) == 0: + downnodes.append(node) + + self.CM.fencing_cleanup("NearQuorumPoint", stonith) + if upnodes == [] and downnodes == []: + self.CM.cluster_stable() + + # Make sure they're completely down with no residule + for node in stopset: + self.rsh(node, self.templates["StopCmd"]) + + return self.success() + + if len(upnodes) > 0: + self.logger.log("Warn: Unstoppable nodes: " + repr(upnodes)) + + if len(downnodes) > 0: + self.logger.log("Warn: Unstartable nodes: " + repr(downnodes)) + + return self.failure() + + def is_applicable(self): + return True + +AllTestClasses.append(NearQuorumPointTest) + + +class RollingUpgradeTest(CTSTest): + '''Perform a rolling upgrade of the cluster''' + def __init__(self, cm): + CTSTest.__init__(self,cm) + self.name = "RollingUpgrade" + self.start = StartTest(cm) + self.stop = StopTest(cm) + self.stopall = SimulStopLite(cm) + self.startall = SimulStartLite(cm) + + def setup(self, node): + # Start all remaining nodes + ret = self.stopall(None) + if not ret: + return self.failure("Couldn't stop all nodes") + + for node in self.Env["nodes"]: + if not self.downgrade(node, None): + return self.failure("Couldn't downgrade %s" % node) + + ret = self.startall(None) + if not ret: + return self.failure("Couldn't start all nodes") + return self.success() + + def teardown(self, node): + # Stop everything + ret = self.stopall(None) + if not ret: + return self.failure("Couldn't stop all nodes") + + for node in self.Env["nodes"]: + if not self.upgrade(node, None): + return self.failure("Couldn't upgrade %s" % node) + + return self.success() + + def install(self, node, version, start=1, flags="--force"): + + target_dir = "/tmp/rpm-%s" % version + src_dir = "%s/%s" % (self.Env["rpm-dir"], version) + + self.logger.log("Installing %s on %s with %s" % (version, node, flags)) + if not self.stop(node): + return self.failure("stop failure: "+node) + + self.rsh(node, "mkdir -p %s" % target_dir) + self.rsh(node, "rm -f %s/*.rpm" % target_dir) + (_, lines) = self.rsh(node, "ls -1 %s/*.rpm" % src_dir, verbose=1) + for line in lines: + line = line[:-1] + rc = self.rsh.copy("%s" % (line), "%s:%s/" % (node, target_dir)) + self.rsh(node, "rpm -Uvh %s %s/*.rpm" % (flags, target_dir)) + + if start and not self.start(node): + return self.failure("start failure: "+node) + + return self.success() + + def upgrade(self, node, start=1): + return self.install(node, self.Env["current-version"], start) + + def downgrade(self, node, start=1): + return self.install(node, self.Env["previous-version"], start, "--force --nodeps") + + def __call__(self, node): + '''Perform the 'Rolling Upgrade' test. ''' + self.incr("calls") + + for node in self.Env["nodes"]: + if self.upgrade(node): + return self.failure("Couldn't upgrade %s" % node) + + self.CM.cluster_stable() + + return self.success() + + def is_applicable(self): + if not self.is_applicable_common(): + return None + + if not "rpm-dir" in list(self.Env.keys()): + return None + if not "current-version" in list(self.Env.keys()): + return None + if not "previous-version" in list(self.Env.keys()): + return None + + return 1 + +# Register RestartTest as a good test to run +AllTestClasses.append(RollingUpgradeTest) + + +class BSC_AddResource(CTSTest): + '''Add a resource to the cluster''' + def __init__(self, cm): + CTSTest.__init__(self, cm) + self.name = "AddResource" + self.resource_offset = 0 + self.cib_cmd = """cibadmin -C -o %s -X '%s' """ + + def __call__(self, node): + self.incr("calls") + self.resource_offset = self.resource_offset + 1 + + r_id = "bsc-rsc-%s-%d" % (node, self.resource_offset) + start_pat = "pacemaker-controld.*%s_start_0.*confirmed.*ok" + + patterns = [] + patterns.append(start_pat % r_id) + + watch = self.create_watch(patterns, self.Env["DeadTime"]) + watch.set_watch() + + ip = self.NextIP() + if not self.make_ip_resource(node, r_id, "ocf", "IPaddr", ip): + return self.failure("Make resource %s failed" % r_id) + + failed = 0 + watch_result = watch.look_for_all() + if watch.unmatched: + for regex in watch.unmatched: + self.logger.log ("Warn: Pattern not found: %s" % (regex)) + failed = 1 + + if failed: + return self.failure("Resource pattern(s) not found") + + if not self.CM.cluster_stable(self.Env["DeadTime"]): + return self.failure("Unstable cluster") + + return self.success() + + def NextIP(self): + ip = self.Env["IPBase"] + if ":" in ip: + fields = ip.rpartition(":") + fields[2] = str(hex(int(fields[2], 16)+1)) + print(str(hex(int(f[2], 16)+1))) + else: + fields = ip.rpartition('.') + fields[2] = str(int(fields[2])+1) + + ip = fields[0] + fields[1] + fields[3]; + self.Env["IPBase"] = ip + return ip.strip() + + def make_ip_resource(self, node, id, rclass, type, ip): + self.logger.log("Creating %s:%s:%s (%s) on %s" % (rclass,type,id,ip,node)) + rsc_xml=""" +<primitive id="%s" class="%s" type="%s" provider="heartbeat"> + <instance_attributes id="%s"><attributes> + <nvpair id="%s" name="ip" value="%s"/> + </attributes></instance_attributes> +</primitive>""" % (id, rclass, type, id, id, ip) + + node_constraint = """ + <rsc_location id="run_%s" rsc="%s"> + <rule id="pref_run_%s" score="100"> + <expression id="%s_loc_expr" attribute="#uname" operation="eq" value="%s"/> + </rule> + </rsc_location>""" % (id, id, id, id, node) + + rc = 0 + (rc, _) = self.rsh(node, self.cib_cmd % ("constraints", node_constraint), verbose=1) + if rc != 0: + self.logger.log("Constraint creation failed: %d" % rc) + return None + + (rc, _) = self.rsh(node, self.cib_cmd % ("resources", rsc_xml), verbose=1) + if rc != 0: + self.logger.log("Resource creation failed: %d" % rc) + return None + + return 1 + + def is_applicable(self): + if self.Env["DoBSC"]: + return True + return None + +AllTestClasses.append(BSC_AddResource) + + +class SimulStopLite(CTSTest): + '''Stop any active nodes ~ simultaneously''' + def __init__(self, cm): + CTSTest.__init__(self,cm) + self.name = "SimulStopLite" + + def __call__(self, dummy): + '''Perform the 'SimulStopLite' setup work. ''' + self.incr("calls") + + self.debug("Setup: " + self.name) + + # We ignore the "node" parameter... + watchpats = [ ] + + for node in self.Env["nodes"]: + if self.CM.ShouldBeStatus[node] == "up": + self.incr("WasStarted") + watchpats.append(self.templates["Pat:We_stopped"] % node) + + if len(watchpats) == 0: + return self.success() + + # Stop all the nodes - at about the same time... + watch = self.create_watch(watchpats, self.Env["DeadTime"]+10) + + watch.set_watch() + self.set_timer() + for node in self.Env["nodes"]: + if self.CM.ShouldBeStatus[node] == "up": + self.CM.StopaCMnoBlock(node) + if watch.look_for_all(): + # Make sure they're completely down with no residule + for node in self.Env["nodes"]: + self.rsh(node, self.templates["StopCmd"]) + + return self.success() + + did_fail = 0 + up_nodes = [] + for node in self.Env["nodes"]: + if self.CM.StataCM(node) == 1: + did_fail = 1 + up_nodes.append(node) + + if did_fail: + return self.failure("Active nodes exist: " + repr(up_nodes)) + + self.logger.log("Warn: All nodes stopped but CTS didn't detect: " + + repr(watch.unmatched)) + + return self.failure("Missing log message: "+repr(watch.unmatched)) + + def is_applicable(self): + '''SimulStopLite is a setup test and never applicable''' + return False + + +class SimulStartLite(CTSTest): + '''Start any stopped nodes ~ simultaneously''' + def __init__(self, cm): + CTSTest.__init__(self,cm) + self.name = "SimulStartLite" + + def __call__(self, dummy): + '''Perform the 'SimulStartList' setup work. ''' + self.incr("calls") + self.debug("Setup: " + self.name) + + # We ignore the "node" parameter... + node_list = [] + for node in self.Env["nodes"]: + if self.CM.ShouldBeStatus[node] == "down": + self.incr("WasStopped") + node_list.append(node) + + self.set_timer() + while len(node_list) > 0: + # Repeat until all nodes come up + watchpats = [ ] + + uppat = self.templates["Pat:NonDC_started"] + if self.CM.upcount() == 0: + uppat = self.templates["Pat:Local_started"] + + watchpats.append(self.templates["Pat:DC_IDLE"]) + for node in node_list: + watchpats.append(uppat % node) + watchpats.append(self.templates["Pat:InfraUp"] % node) + watchpats.append(self.templates["Pat:PacemakerUp"] % node) + + # Start all the nodes - at about the same time... + watch = self.create_watch(watchpats, self.Env["DeadTime"]+10) + watch.set_watch() + + stonith = self.CM.prepare_fencing_watcher(self.name) + + for node in node_list: + self.CM.StartaCMnoBlock(node) + + watch.look_for_all() + + node_list = self.CM.fencing_cleanup(self.name, stonith) + + if node_list == None: + return self.failure("Cluster did not stabilize") + + # Remove node_list messages from watch.unmatched + for node in node_list: + self.logger.debug("Dealing with stonith operations for %s" % repr(node_list)) + if watch.unmatched: + try: + watch.unmatched.remove(uppat % node) + except: + self.debug("Already matched: %s" % (uppat % node)) + try: + watch.unmatched.remove(self.templates["Pat:InfraUp"] % node) + except: + self.debug("Already matched: %s" % (self.templates["Pat:InfraUp"] % node)) + try: + watch.unmatched.remove(self.templates["Pat:PacemakerUp"] % node) + except: + self.debug("Already matched: %s" % (self.templates["Pat:PacemakerUp"] % node)) + + if watch.unmatched: + for regex in watch.unmatched: + self.logger.log ("Warn: Startup pattern not found: %s" %(regex)) + + if not self.CM.cluster_stable(): + return self.failure("Cluster did not stabilize") + + did_fail = 0 + unstable = [] + for node in self.Env["nodes"]: + if self.CM.StataCM(node) == 0: + did_fail = 1 + unstable.append(node) + + if did_fail: + return self.failure("Unstarted nodes exist: " + repr(unstable)) + + unstable = [] + for node in self.Env["nodes"]: + if not self.CM.node_stable(node): + did_fail = 1 + unstable.append(node) + + if did_fail: + return self.failure("Unstable cluster nodes exist: " + repr(unstable)) + + return self.success() + + def is_applicable(self): + '''SimulStartLite is a setup test and never applicable''' + return False + + +def TestList(cm, audits): + result = [] + for testclass in AllTestClasses: + bound_test = testclass(cm) + if bound_test.is_applicable(): + bound_test.Audits = audits + result.append(bound_test) + return result + + +class RemoteLXC(CTSTest): + def __init__(self, cm): + CTSTest.__init__(self,cm) + self.name = "RemoteLXC" + self.start = StartTest(cm) + self.startall = SimulStartLite(cm) + self.num_containers = 2 + self.is_container = 1 + self.failed = 0 + self.fail_string = "" + + def start_lxc_simple(self, node): + + # restore any artifacts laying around from a previous test. + self.rsh(node, "/usr/share/pacemaker/tests/cts/lxc_autogen.sh -s -R &>/dev/null") + + # generate the containers, put them in the config, add some resources to them + pats = [ ] + watch = self.create_watch(pats, 120) + watch.set_watch() + pats.append(self.templates["Pat:RscOpOK"] % ("start", "lxc1")) + pats.append(self.templates["Pat:RscOpOK"] % ("start", "lxc2")) + pats.append(self.templates["Pat:RscOpOK"] % ("start", "lxc-ms")) + pats.append(self.templates["Pat:RscOpOK"] % ("promote", "lxc-ms")) + + self.rsh(node, "/usr/share/pacemaker/tests/cts/lxc_autogen.sh -g -a -m -s -c %d &>/dev/null" % self.num_containers) + self.set_timer("remoteSimpleInit") + watch.look_for_all() + self.log_timer("remoteSimpleInit") + if watch.unmatched: + self.fail_string = "Unmatched patterns: %s" % (repr(watch.unmatched)) + self.failed = 1 + + def cleanup_lxc_simple(self, node): + + pats = [ ] + # if the test failed, attempt to clean up the cib and libvirt environment + # as best as possible + if self.failed == 1: + # restore libvirt and cib + self.rsh(node, "/usr/share/pacemaker/tests/cts/lxc_autogen.sh -s -R &>/dev/null") + return + + watch = self.create_watch(pats, 120) + watch.set_watch() + + pats.append(self.templates["Pat:RscOpOK"] % ("stop", "container1")) + pats.append(self.templates["Pat:RscOpOK"] % ("stop", "container2")) + + self.rsh(node, "/usr/share/pacemaker/tests/cts/lxc_autogen.sh -p &>/dev/null") + self.set_timer("remoteSimpleCleanup") + watch.look_for_all() + self.log_timer("remoteSimpleCleanup") + + if watch.unmatched: + self.fail_string = "Unmatched patterns: %s" % (repr(watch.unmatched)) + self.failed = 1 + + # cleanup libvirt + self.rsh(node, "/usr/share/pacemaker/tests/cts/lxc_autogen.sh -s -R &>/dev/null") + + def __call__(self, node): + '''Perform the 'RemoteLXC' test. ''' + self.incr("calls") + + ret = self.startall(None) + if not ret: + return self.failure("Setup failed, start all nodes failed.") + + (rc, _) = self.rsh(node, "/usr/share/pacemaker/tests/cts/lxc_autogen.sh -v &>/dev/null") + if rc == 1: + self.log("Environment test for lxc support failed.") + return self.skipped() + + self.start_lxc_simple(node) + self.cleanup_lxc_simple(node) + + self.debug("Waiting for the cluster to recover") + self.CM.cluster_stable() + + if self.failed == 1: + return self.failure(self.fail_string) + + return self.success() + + def errorstoignore(self): + '''Return list of errors which should be ignored''' + return [ + r"Updating failcount for ping", + r"schedulerd.*: Recover\s+(ping|lxc-ms|container)\s+\(.*\)", + # The orphaned lxc-ms resource causes an expected transition error + # that is a result of the scheduler not having knowledge that the + # promotable resource used to be a clone. As a result, it looks like that + # resource is running in multiple locations when it shouldn't... But in + # this instance we know why this error is occurring and that it is expected. + r"Calculated [Tt]ransition .*pe-error", + r"Resource lxc-ms .* is active on 2 nodes attempting recovery", + r"Unknown operation: fail", + r"VirtualDomain.*ERROR: Unable to determine emulator", + ] + +AllTestClasses.append(RemoteLXC) + + +class RemoteDriver(CTSTest): + + def __init__(self, cm): + CTSTest.__init__(self,cm) + self.name = self.__class__.__name__ + self.start = StartTest(cm) + self.startall = SimulStartLite(cm) + self.stop = StopTest(cm) + self.remote_rsc = "remote-rsc" + self.cib_cmd = """cibadmin -C -o %s -X '%s' """ + self.reset() + + def reset(self): + self.pcmk_started = 0 + self.failed = False + self.fail_string = "" + self.remote_node_added = 0 + self.remote_rsc_added = 0 + self.remote_use_reconnect_interval = self.Env.random_gen.choice([True,False]) + + def fail(self, msg): + """ Mark test as failed. """ + + self.failed = True + + # Always log the failure. + self.logger.log(msg) + + # Use first failure as test status, as it's likely to be most useful. + if not self.fail_string: + self.fail_string = msg + + def get_othernode(self, node): + for othernode in self.Env["nodes"]: + if othernode == node: + # we don't want to try and use the cib that we just shutdown. + # find a cluster node that is not our soon to be remote-node. + continue + else: + return othernode + + def del_rsc(self, node, rsc): + othernode = self.get_othernode(node) + (rc, _) = self.rsh(othernode, "crm_resource -D -r %s -t primitive" % (rsc)) + if rc != 0: + self.fail("Removal of resource '%s' failed" % rsc) + + def add_rsc(self, node, rsc_xml): + othernode = self.get_othernode(node) + (rc, _) = self.rsh(othernode, self.cib_cmd % ("resources", rsc_xml)) + if rc != 0: + self.fail("resource creation failed") + + def add_primitive_rsc(self, node): + rsc_xml = """ +<primitive class="ocf" id="%(node)s" provider="heartbeat" type="Dummy"> + <meta_attributes id="%(node)s-meta_attributes"/> + <operations> + <op id="%(node)s-monitor-interval-20s" interval="20s" name="monitor"/> + </operations> +</primitive>""" % { "node": self.remote_rsc } + self.add_rsc(node, rsc_xml) + if not self.failed: + self.remote_rsc_added = 1 + + def add_connection_rsc(self, node): + rsc_xml = """ +<primitive class="ocf" id="%(node)s" provider="pacemaker" type="remote"> + <instance_attributes id="%(node)s-instance_attributes"> + <nvpair id="%(node)s-instance_attributes-server" name="server" value="%(server)s"/> +""" % { "node": self.remote_node, "server": node } + + if self.remote_use_reconnect_interval: + # Set reconnect interval on resource + rsc_xml = rsc_xml + """ + <nvpair id="%s-instance_attributes-reconnect_interval" name="reconnect_interval" value="60s"/> +""" % (self.remote_node) + + rsc_xml = rsc_xml + """ + </instance_attributes> + <operations> + <op id="%(node)s-start" name="start" interval="0" timeout="120s"/> + <op id="%(node)s-monitor-20s" name="monitor" interval="20s" timeout="45s"/> + </operations> +</primitive> +""" % { "node": self.remote_node } + + self.add_rsc(node, rsc_xml) + if not self.failed: + self.remote_node_added = 1 + + def disable_services(self, node): + self.corosync_enabled = self.Env.service_is_enabled(node, "corosync") + if self.corosync_enabled: + self.Env.disable_service(node, "corosync") + + self.pacemaker_enabled = self.Env.service_is_enabled(node, "pacemaker") + if self.pacemaker_enabled: + self.Env.disable_service(node, "pacemaker") + + def restore_services(self, node): + if self.corosync_enabled: + self.Env.enable_service(node, "corosync") + + if self.pacemaker_enabled: + self.Env.enable_service(node, "pacemaker") + + def stop_pcmk_remote(self, node): + # disable pcmk remote + for i in range(10): + (rc, _) = self.rsh(node, "service pacemaker_remote stop") + if rc != 0: + time.sleep(6) + else: + break + + def start_pcmk_remote(self, node): + for i in range(10): + (rc, _) = self.rsh(node, "service pacemaker_remote start") + if rc != 0: + time.sleep(6) + else: + self.pcmk_started = 1 + break + + def freeze_pcmk_remote(self, node): + """ Simulate a Pacemaker Remote daemon failure. """ + + # We freeze the process. + self.rsh(node, "killall -STOP pacemaker-remoted") + + def resume_pcmk_remote(self, node): + # We resume the process. + self.rsh(node, "killall -CONT pacemaker-remoted") + + def start_metal(self, node): + # Cluster nodes are reused as remote nodes in remote tests. If cluster + # services were enabled at boot, in case the remote node got fenced, the + # cluster node would join instead of the expected remote one. Meanwhile + # pacemaker_remote would not be able to start. Depending on the chances, + # the situations might not be able to be orchestrated gracefully any more. + # + # Temporarily disable any enabled cluster serivces. + self.disable_services(node) + + pcmk_started = 0 + + # make sure the resource doesn't already exist for some reason + self.rsh(node, "crm_resource -D -r %s -t primitive" % (self.remote_rsc)) + self.rsh(node, "crm_resource -D -r %s -t primitive" % (self.remote_node)) + + if not self.stop(node): + self.fail("Failed to shutdown cluster node %s" % node) + return + + self.start_pcmk_remote(node) + + if self.pcmk_started == 0: + self.fail("Failed to start pacemaker_remote on node %s" % node) + return + + # Convert node to baremetal now that it has shutdown the cluster stack + pats = [ ] + watch = self.create_watch(pats, 120) + watch.set_watch() + pats.append(self.templates["Pat:RscOpOK"] % ("start", self.remote_node)) + pats.append(self.templates["Pat:DC_IDLE"]) + + self.add_connection_rsc(node) + + self.set_timer("remoteMetalInit") + watch.look_for_all() + self.log_timer("remoteMetalInit") + if watch.unmatched: + self.fail("Unmatched patterns: %s" % watch.unmatched) + + def migrate_connection(self, node): + if self.failed: + return + + pats = [ ] + pats.append(self.templates["Pat:RscOpOK"] % ("migrate_to", self.remote_node)) + pats.append(self.templates["Pat:RscOpOK"] % ("migrate_from", self.remote_node)) + pats.append(self.templates["Pat:DC_IDLE"]) + watch = self.create_watch(pats, 120) + watch.set_watch() + + (rc, _) = self.rsh(node, "crm_resource -M -r %s" % (self.remote_node), verbose=1) + if rc != 0: + self.fail("failed to move remote node connection resource") + return + + self.set_timer("remoteMetalMigrate") + watch.look_for_all() + self.log_timer("remoteMetalMigrate") + + if watch.unmatched: + self.fail("Unmatched patterns: %s" % watch.unmatched) + return + + def fail_rsc(self, node): + if self.failed: + return + + watchpats = [ ] + watchpats.append(self.templates["Pat:RscRemoteOpOK"] % ("stop", self.remote_rsc, self.remote_node)) + watchpats.append(self.templates["Pat:RscRemoteOpOK"] % ("start", self.remote_rsc, self.remote_node)) + watchpats.append(self.templates["Pat:DC_IDLE"]) + + watch = self.create_watch(watchpats, 120) + watch.set_watch() + + self.debug("causing dummy rsc to fail.") + + self.rsh(node, "rm -f /var/run/resource-agents/Dummy*") + + self.set_timer("remoteRscFail") + watch.look_for_all() + self.log_timer("remoteRscFail") + if watch.unmatched: + self.fail("Unmatched patterns during rsc fail: %s" % watch.unmatched) + + def fail_connection(self, node): + if self.failed: + return + + watchpats = [ ] + watchpats.append(self.templates["Pat:Fencing_ok"] % self.remote_node) + watchpats.append(self.templates["Pat:NodeFenced"] % self.remote_node) + + watch = self.create_watch(watchpats, 120) + watch.set_watch() + + # freeze the pcmk remote daemon. this will result in fencing + self.debug("Force stopped active remote node") + self.freeze_pcmk_remote(node) + + self.debug("Waiting for remote node to be fenced.") + self.set_timer("remoteMetalFence") + watch.look_for_all() + self.log_timer("remoteMetalFence") + if watch.unmatched: + self.fail("Unmatched patterns: %s" % watch.unmatched) + return + + self.debug("Waiting for the remote node to come back up") + self.CM.ns.wait_for_node(node, 120); + + pats = [ ] + watch = self.create_watch(pats, 240) + watch.set_watch() + pats.append(self.templates["Pat:RscOpOK"] % ("start", self.remote_node)) + if self.remote_rsc_added == 1: + pats.append(self.templates["Pat:RscRemoteOpOK"] % ("start", self.remote_rsc, self.remote_node)) + + # start the remote node again watch it integrate back into cluster. + self.start_pcmk_remote(node) + if self.pcmk_started == 0: + self.fail("Failed to start pacemaker_remote on node %s" % node) + return + + self.debug("Waiting for remote node to rejoin cluster after being fenced.") + self.set_timer("remoteMetalRestart") + watch.look_for_all() + self.log_timer("remoteMetalRestart") + if watch.unmatched: + self.fail("Unmatched patterns: %s" % watch.unmatched) + return + + def add_dummy_rsc(self, node): + if self.failed: + return + + # verify we can put a resource on the remote node + pats = [ ] + watch = self.create_watch(pats, 120) + watch.set_watch() + pats.append(self.templates["Pat:RscRemoteOpOK"] % ("start", self.remote_rsc, self.remote_node)) + pats.append(self.templates["Pat:DC_IDLE"]) + + # Add a resource that must live on remote-node + self.add_primitive_rsc(node) + + # force that rsc to prefer the remote node. + (rc, _) = self.CM.rsh(node, "crm_resource -M -r %s -N %s -f" % (self.remote_rsc, self.remote_node), verbose=1) + if rc != 0: + self.fail("Failed to place remote resource on remote node.") + return + + self.set_timer("remoteMetalRsc") + watch.look_for_all() + self.log_timer("remoteMetalRsc") + if watch.unmatched: + self.fail("Unmatched patterns: %s" % watch.unmatched) + + def test_attributes(self, node): + if self.failed: + return + + # This verifies permanent attributes can be set on a remote-node. It also + # verifies the remote-node can edit its own cib node section remotely. + (rc, line) = self.CM.rsh(node, "crm_attribute -l forever -n testattr -v testval -N %s" % (self.remote_node), verbose=1) + if rc != 0: + self.fail("Failed to set remote-node attribute. rc:%s output:%s" % (rc, line)) + return + + (rc, _) = self.CM.rsh(node, "crm_attribute -l forever -n testattr -q -N %s" % (self.remote_node), verbose=1) + if rc != 0: + self.fail("Failed to get remote-node attribute") + return + + (rc, _) = self.CM.rsh(node, "crm_attribute -l forever -n testattr -D -N %s" % (self.remote_node), verbose=1) + if rc != 0: + self.fail("Failed to delete remote-node attribute") + return + + def cleanup_metal(self, node): + self.restore_services(node) + + if self.pcmk_started == 0: + return + + pats = [ ] + + watch = self.create_watch(pats, 120) + watch.set_watch() + + if self.remote_rsc_added == 1: + pats.append(self.templates["Pat:RscOpOK"] % ("stop", self.remote_rsc)) + if self.remote_node_added == 1: + pats.append(self.templates["Pat:RscOpOK"] % ("stop", self.remote_node)) + + self.set_timer("remoteMetalCleanup") + + self.resume_pcmk_remote(node) + + if self.remote_rsc_added == 1: + + # Remove dummy resource added for remote node tests + self.debug("Cleaning up dummy rsc put on remote node") + self.rsh(self.get_othernode(node), "crm_resource -U -r %s" % self.remote_rsc) + self.del_rsc(node, self.remote_rsc) + + if self.remote_node_added == 1: + + # Remove remote node's connection resource + self.debug("Cleaning up remote node connection resource") + self.rsh(self.get_othernode(node), "crm_resource -U -r %s" % (self.remote_node)) + self.del_rsc(node, self.remote_node) + + watch.look_for_all() + self.log_timer("remoteMetalCleanup") + + if watch.unmatched: + self.fail("Unmatched patterns: %s" % watch.unmatched) + + self.stop_pcmk_remote(node) + + self.debug("Waiting for the cluster to recover") + self.CM.cluster_stable() + + if self.remote_node_added == 1: + # Remove remote node itself + self.debug("Cleaning up node entry for remote node") + self.rsh(self.get_othernode(node), "crm_node --force --remove %s" % self.remote_node) + + def setup_env(self, node): + + self.remote_node = "remote-%s" % (node) + + # we are assuming if all nodes have a key, that it is + # the right key... If any node doesn't have a remote + # key, we regenerate it everywhere. + if self.rsh.exists_on_all("/etc/pacemaker/authkey", self.Env["nodes"]): + return + + # create key locally + (handle, keyfile) = tempfile.mkstemp(".cts") + os.close(handle) + subprocess.check_call(["dd", "if=/dev/urandom", "of=%s" % keyfile, "bs=4096", "count=1"], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + # sync key throughout the cluster + for node in self.Env["nodes"]: + self.rsh(node, "mkdir -p --mode=0750 /etc/pacemaker") + self.rsh.copy(keyfile, "root@%s:/etc/pacemaker/authkey" % node) + self.rsh(node, "chgrp haclient /etc/pacemaker /etc/pacemaker/authkey") + self.rsh(node, "chmod 0640 /etc/pacemaker/authkey") + os.unlink(keyfile) + + def is_applicable(self): + if not self.is_applicable_common(): + return False + + for node in self.Env["nodes"]: + (rc, _) = self.rsh(node, "which pacemaker-remoted >/dev/null 2>&1") + if rc != 0: + return False + return True + + def start_new_test(self, node): + self.incr("calls") + self.reset() + + ret = self.startall(None) + if not ret: + return self.failure("setup failed: could not start all nodes") + + self.setup_env(node) + self.start_metal(node) + self.add_dummy_rsc(node) + return True + + def __call__(self, node): + return self.failure("This base class is not meant to be called directly.") + + def errorstoignore(self): + '''Return list of errors which should be ignored''' + return [ r"""is running on remote.*which isn't allowed""", + r"""Connection terminated""", + r"""Could not send remote""", + ] + +# RemoteDriver is just a base class for other tests, so it is not added to AllTestClasses + + +class RemoteBasic(RemoteDriver): + + def __call__(self, node): + '''Perform the 'RemoteBaremetal' test. ''' + + if not self.start_new_test(node): + return self.failure(self.fail_string) + + self.test_attributes(node) + self.cleanup_metal(node) + + self.debug("Waiting for the cluster to recover") + self.CM.cluster_stable() + if self.failed: + return self.failure(self.fail_string) + + return self.success() + +AllTestClasses.append(RemoteBasic) + +class RemoteStonithd(RemoteDriver): + + def __call__(self, node): + '''Perform the 'RemoteStonithd' test. ''' + + if not self.start_new_test(node): + return self.failure(self.fail_string) + + self.fail_connection(node) + self.cleanup_metal(node) + + self.debug("Waiting for the cluster to recover") + self.CM.cluster_stable() + if self.failed: + return self.failure(self.fail_string) + + return self.success() + + def is_applicable(self): + if not RemoteDriver.is_applicable(self): + return False + + if "DoFencing" in list(self.Env.keys()): + return self.Env["DoFencing"] + + return True + + def errorstoignore(self): + ignore_pats = [ + r"Lost connection to Pacemaker Remote node", + r"Software caused connection abort", + r"pacemaker-controld.*:\s+error.*: Operation remote-.*_monitor", + r"pacemaker-controld.*:\s+error.*: Result of monitor operation for remote-.*", + r"schedulerd.*:\s+Recover\s+remote-.*\s+\(.*\)", + r"error: Result of monitor operation for .* on remote-.*: Internal communication failure", + ] + + ignore_pats.extend(RemoteDriver.errorstoignore(self)) + return ignore_pats + +AllTestClasses.append(RemoteStonithd) + + +class RemoteMigrate(RemoteDriver): + + def __call__(self, node): + '''Perform the 'RemoteMigrate' test. ''' + + if not self.start_new_test(node): + return self.failure(self.fail_string) + + self.migrate_connection(node) + self.cleanup_metal(node) + + self.debug("Waiting for the cluster to recover") + self.CM.cluster_stable() + if self.failed: + return self.failure(self.fail_string) + + return self.success() + + def is_applicable(self): + if not RemoteDriver.is_applicable(self): + return 0 + # This test requires at least three nodes: one to convert to a + # remote node, one to host the connection originally, and one + # to migrate the connection to. + if len(self.Env["nodes"]) < 3: + return 0 + return 1 + +AllTestClasses.append(RemoteMigrate) + + +class RemoteRscFailure(RemoteDriver): + + def __call__(self, node): + '''Perform the 'RemoteRscFailure' test. ''' + + if not self.start_new_test(node): + return self.failure(self.fail_string) + + # This is an important step. We are migrating the connection + # before failing the resource. This verifies that the migration + # has properly maintained control over the remote-node. + self.migrate_connection(node) + + self.fail_rsc(node) + self.cleanup_metal(node) + + self.debug("Waiting for the cluster to recover") + self.CM.cluster_stable() + if self.failed: + return self.failure(self.fail_string) + + return self.success() + + def errorstoignore(self): + ignore_pats = [ + r"schedulerd.*: Recover\s+remote-rsc\s+\(.*\)", + r"Dummy.*: No process state file found", + ] + + ignore_pats.extend(RemoteDriver.errorstoignore(self)) + return ignore_pats + + def is_applicable(self): + if not RemoteDriver.is_applicable(self): + return 0 + # This test requires at least three nodes: one to convert to a + # remote node, one to host the connection originally, and one + # to migrate the connection to. + if len(self.Env["nodes"]) < 3: + return 0 + return 1 + +AllTestClasses.append(RemoteRscFailure) + +# vim:ts=4:sw=4:et: |