diff --git a/lib/backend.py b/lib/backend.py index 88f5296d1a48bfaba4bc8903c8b8b1f044fc6575..b92be86284922e022bd3715f12f4a0367495aee3 100644 --- a/lib/backend.py +++ b/lib/backend.py @@ -40,7 +40,6 @@ import time import stat import errno import re -import subprocess import random import logging import tempfile @@ -2666,40 +2665,28 @@ class HooksRunner(object): else: _Fail("Unknown hooks phase '%s'", phase) - rr = [] subdir = "%s-%s.d" % (hpath, suffix) dir_name = "%s/%s" % (self._BASE_DIR, subdir) - try: - dir_contents = utils.ListVisibleFiles(dir_name) - except OSError: - # FIXME: must log output in case of failures - return rr - - # we use the standard python sort order, - # so 00name is the recommended naming scheme - dir_contents.sort() - for relname in dir_contents: - fname = os.path.join(dir_name, relname) - if not (os.path.isfile(fname) and os.access(fname, os.X_OK) and - constants.EXT_PLUGIN_MASK.match(relname) is not None): + runparts_results = utils.RunParts(dir_name, env=env, reset_env=True) + + results = [] + for (relname, relstatus, runresult) in runparts_results: + if relstatus == constants.RUNPARTS_SKIP: rrval = constants.HKR_SKIP output = "" - else: - try: - result = utils.RunCmd([fname], env=env, reset_env=True) - except (OpExecError, EnvironmentError), err: + elif relstatus == constants.RUNPARTS_ERR: + rrval = constants.HKR_FAIL + output = "Hook script execution error: %s" % runresult + elif relstatus == constants.RUNPARTS_RUN: + if runresult.failed: rrval = constants.HKR_FAIL - output = "Hook script error: %s" % str(err) else: - if result.failed: - rrval = constants.HKR_FAIL - else: - rrval = constants.HKR_SUCCESS - output = utils.SafeEncode(result.output.strip()) - rr.append(("%s/%s" % (subdir, relname), rrval, output)) - - return rr + rrval = constants.HKR_SUCCESS + output = utils.SafeEncode(runresult.output.strip()) + results.append(("%s/%s" % (subdir, relname), rrval, output)) + + return results class IAllocatorRunner(object): diff --git a/lib/constants.py b/lib/constants.py index 47849fcb756aed2ab0322ca7970ac9423500d490..4fff9e7133b0ec34a1a1acbe3d33de3faca1af8e 100644 --- a/lib/constants.py +++ b/lib/constants.py @@ -335,6 +335,13 @@ LVM_STRIPECOUNT = _autoconf.LVM_STRIPECOUNT DEFAULT_SHUTDOWN_TIMEOUT = 120 NODE_MAX_CLOCK_SKEW = 150 +# runparts results +(RUNPARTS_SKIP, + RUNPARTS_RUN, + RUNPARTS_ERR) = range(3) + +RUNPARTS_STATUS = frozenset([RUNPARTS_SKIP, RUNPARTS_RUN, RUNPARTS_ERR]) + # RPC constants (RPC_ENCODING_NONE, RPC_ENCODING_ZLIB_BASE64) = range(2) diff --git a/lib/utils.py b/lib/utils.py index 261c52c2ba90b5e9b6b8d046e66af854c61ca463..ec08b1eb6b78c4b4ca1589ceff1b0f91df9e139b 100644 --- a/lib/utils.py +++ b/lib/utils.py @@ -288,6 +288,43 @@ def _RunCmdFile(cmd, env, via_shell, output, cwd): return status +def RunParts(dir_name, env=None, reset_env=False): + """Run Scripts or programs in a directory + + @type dir_name: string + @param dir_name: absolute path to a directory + @type env: dict + @param env: The environment to use + @type reset_env: boolean + @param reset_env: whether to reset or keep the default os environment + @rtype: list of tuples + @return: list of (name, (one of RUNDIR_STATUS), RunResult) + + """ + rr = [] + + try: + dir_contents = ListVisibleFiles(dir_name) + except OSError, err: + logging.warning("RunParts: skipping %s (cannot list: %s)", dir_name, err) + return rr + + for relname in sorted(dir_contents): + fname = os.path.join(dir_name, relname) + if not (os.path.isfile(fname) and os.access(fname, os.X_OK) and + constants.EXT_PLUGIN_MASK.match(relname) is not None): + rr.append((relname, constants.RUNPARTS_SKIP, None)) + else: + try: + result = RunCmd([fname], env=env, reset_env=reset_env) + except Exception, err: # pylint: disable-msg=W0703 + rr.append((relname, constants.RUNPARTS_ERR, str(err))) + else: + rr.append((relname, constants.RUNPARTS_RUN, result)) + + return rr + + def RemoveFile(filename): """Remove a file ignoring some errors. diff --git a/test/ganeti.utils_unittest.py b/test/ganeti.utils_unittest.py index 906aecabf060203de8a37341ccf0b2417a86d489..4d19f859985419a5ef19c7beacc20fa18348c26e 100755 --- a/test/ganeti.utils_unittest.py +++ b/test/ganeti.utils_unittest.py @@ -27,6 +27,7 @@ import time import tempfile import os.path import os +import stat import md5 import signal import socket @@ -46,7 +47,7 @@ from ganeti.utils import IsProcessAlive, RunCmd, \ ShellQuote, ShellQuoteArgs, TcpPing, ListVisibleFiles, \ SetEtcHostsEntry, RemoveEtcHostsEntry, FirstFree, OwnIpAddress, \ TailFile, ForceDictType, SafeEncode, IsNormAbsPath, FormatTime, \ - UnescapeAndSplit + UnescapeAndSplit, RunParts from ganeti.errors import LockError, UnitParseError, GenericError, \ ProgrammerError @@ -236,6 +237,132 @@ class TestRunCmd(testutils.GanetiTestCase): self.failUnlessEqual(RunCmd(["env"], reset_env=True).stdout.strip(), "") +class TestRunParts(unittest.TestCase): + """Testing case for the RunParts function""" + + def setUp(self): + self.rundir = tempfile.mkdtemp(prefix="ganeti-test", suffix=".tmp") + + def tearDown(self): + shutil.rmtree(self.rundir) + + def testEmpty(self): + """Test on an empty dir""" + self.failUnlessEqual(RunParts(self.rundir, reset_env=True), []) + + def testSkipWrongName(self): + """Test that wrong files are skipped""" + fname = os.path.join(self.rundir, "00test.dot") + utils.WriteFile(fname, data="") + os.chmod(fname, stat.S_IREAD | stat.S_IEXEC) + relname = os.path.basename(fname) + self.failUnlessEqual(RunParts(self.rundir, reset_env=True), + [(relname, constants.RUNPARTS_SKIP, None)]) + + def testSkipNonExec(self): + """Test that non executable files are skipped""" + fname = os.path.join(self.rundir, "00test") + utils.WriteFile(fname, data="") + relname = os.path.basename(fname) + self.failUnlessEqual(RunParts(self.rundir, reset_env=True), + [(relname, constants.RUNPARTS_SKIP, None)]) + + def testError(self): + """Test error on a broken executable""" + fname = os.path.join(self.rundir, "00test") + utils.WriteFile(fname, data="") + os.chmod(fname, stat.S_IREAD | stat.S_IEXEC) + (relname, status, error) = RunParts(self.rundir, reset_env=True)[0] + self.failUnlessEqual(relname, os.path.basename(fname)) + self.failUnlessEqual(status, constants.RUNPARTS_ERR) + self.failUnless(error) + + def testSorted(self): + """Test executions are sorted""" + files = [] + files.append(os.path.join(self.rundir, "64test")) + files.append(os.path.join(self.rundir, "00test")) + files.append(os.path.join(self.rundir, "42test")) + + for fname in files: + utils.WriteFile(fname, data="") + + results = RunParts(self.rundir, reset_env=True) + + for fname in sorted(files): + self.failUnlessEqual(os.path.basename(fname), results.pop(0)[0]) + + def testOk(self): + """Test correct execution""" + fname = os.path.join(self.rundir, "00test") + utils.WriteFile(fname, data="#!/bin/sh\n\necho -n ciao") + os.chmod(fname, stat.S_IREAD | stat.S_IEXEC) + (relname, status, runresult) = RunParts(self.rundir, reset_env=True)[0] + self.failUnlessEqual(relname, os.path.basename(fname)) + self.failUnlessEqual(status, constants.RUNPARTS_RUN) + self.failUnlessEqual(runresult.stdout, "ciao") + + def testRunFail(self): + """Test correct execution, with run failure""" + fname = os.path.join(self.rundir, "00test") + utils.WriteFile(fname, data="#!/bin/sh\n\nexit 1") + os.chmod(fname, stat.S_IREAD | stat.S_IEXEC) + (relname, status, runresult) = RunParts(self.rundir, reset_env=True)[0] + self.failUnlessEqual(relname, os.path.basename(fname)) + self.failUnlessEqual(status, constants.RUNPARTS_RUN) + self.failUnlessEqual(runresult.exit_code, 1) + self.failUnless(runresult.failed) + + def testRunMix(self): + files = [] + files.append(os.path.join(self.rundir, "00test")) + files.append(os.path.join(self.rundir, "42test")) + files.append(os.path.join(self.rundir, "64test")) + files.append(os.path.join(self.rundir, "99test")) + + files.sort() + + # 1st has errors in execution + utils.WriteFile(files[0], data="#!/bin/sh\n\nexit 1") + os.chmod(files[0], stat.S_IREAD | stat.S_IEXEC) + + # 2nd is skipped + utils.WriteFile(files[1], data="") + + # 3rd cannot execute properly + utils.WriteFile(files[2], data="") + os.chmod(files[2], stat.S_IREAD | stat.S_IEXEC) + + # 4th execs + utils.WriteFile(files[3], data="#!/bin/sh\n\necho -n ciao") + os.chmod(files[3], stat.S_IREAD | stat.S_IEXEC) + + results = RunParts(self.rundir, reset_env=True) + + (relname, status, runresult) = results[0] + self.failUnlessEqual(relname, os.path.basename(files[0])) + self.failUnlessEqual(status, constants.RUNPARTS_RUN) + self.failUnlessEqual(runresult.exit_code, 1) + self.failUnless(runresult.failed) + + (relname, status, runresult) = results[1] + self.failUnlessEqual(relname, os.path.basename(files[1])) + self.failUnlessEqual(status, constants.RUNPARTS_SKIP) + self.failUnlessEqual(runresult, None) + + (relname, status, runresult) = results[2] + self.failUnlessEqual(relname, os.path.basename(files[2])) + self.failUnlessEqual(status, constants.RUNPARTS_ERR) + self.failUnless(runresult) + + (relname, status, runresult) = results[3] + self.failUnlessEqual(relname, os.path.basename(files[3])) + self.failUnlessEqual(status, constants.RUNPARTS_RUN) + self.failUnlessEqual(runresult.output, "ciao") + self.failUnlessEqual(runresult.exit_code, 0) + self.failUnless(not runresult.failed) + + class TestRemoveFile(unittest.TestCase): """Test case for the RemoveFile function"""