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"""