Support arguments in utils.RunInSeparateProcess
[ganeti-local] / lib / backend.py
index 6122acd..367e89c 100644 (file)
 
 @var _ALLOWED_UPLOAD_FILES: denotes which files are accepted in
      the L{UploadFile} function
 
 @var _ALLOWED_UPLOAD_FILES: denotes which files are accepted in
      the L{UploadFile} function
+@var _ALLOWED_CLEAN_DIRS: denotes which directories are accepted
+     in the L{_CleanDirectory} function
 
 """
 
 
 """
 
+# pylint: disable-msg=E1103
+
+# E1103: %s %r has no %r member (but some types could not be
+# inferred), because the _TryOSFromDisk returns either (True, os_obj)
+# or (False, "string") which confuses pylint
+
 
 import os
 import os.path
 
 import os
 import os.path
@@ -34,7 +42,6 @@ import time
 import stat
 import errno
 import re
 import stat
 import errno
 import re
-import subprocess
 import random
 import logging
 import tempfile
 import random
 import logging
 import tempfile
@@ -52,6 +59,11 @@ from ganeti import ssconf
 
 
 _BOOT_ID_PATH = "/proc/sys/kernel/random/boot_id"
 
 
 _BOOT_ID_PATH = "/proc/sys/kernel/random/boot_id"
+_ALLOWED_CLEAN_DIRS = frozenset([
+  constants.DATA_DIR,
+  constants.JOB_QUEUE_ARCHIVE_DIR,
+  constants.QUEUE_DIR,
+  ])
 
 
 class RPCFail(Exception):
 
 
 class RPCFail(Exception):
@@ -138,6 +150,10 @@ def _CleanDirectory(path, exclude=None):
       to the empty list
 
   """
       to the empty list
 
   """
+  if path not in _ALLOWED_CLEAN_DIRS:
+    _Fail("Path passed to _CleanDirectory not in allowed clean targets: '%s'",
+          path)
+
   if not os.path.isdir(path):
     return
   if exclude is None:
   if not os.path.isdir(path):
     return
   if exclude is None:
@@ -147,7 +163,7 @@ def _CleanDirectory(path, exclude=None):
     exclude = [os.path.normpath(i) for i in exclude]
 
   for rel_name in utils.ListVisibleFiles(path):
     exclude = [os.path.normpath(i) for i in exclude]
 
   for rel_name in utils.ListVisibleFiles(path):
-    full_name = os.path.normpath(os.path.join(path, rel_name))
+    full_name = utils.PathJoin(path, rel_name)
     if full_name in exclude:
       continue
     if os.path.isfile(full_name) and not os.path.islink(full_name):
     if full_name in exclude:
       continue
     if os.path.isfile(full_name) and not os.path.islink(full_name):
@@ -255,21 +271,20 @@ def StartMaster(start_daemons, no_voting):
 
   # and now start the master and rapi daemons
   if start_daemons:
 
   # and now start the master and rapi daemons
   if start_daemons:
-    daemons_params = {
-        'ganeti-masterd': [],
-        'ganeti-rapi': [],
-        }
     if no_voting:
     if no_voting:
-      daemons_params['ganeti-masterd'].append('--no-voting')
-      daemons_params['ganeti-masterd'].append('--yes-do-it')
-    for daemon in daemons_params:
-      cmd = [daemon]
-      cmd.extend(daemons_params[daemon])
-      result = utils.RunCmd(cmd)
-      if result.failed:
-        msg = "Can't start daemon %s: %s" % (daemon, result.output)
-        logging.error(msg)
-        err_msgs.append(msg)
+      masterd_args = "--no-voting --yes-do-it"
+    else:
+      masterd_args = ""
+
+    env = {
+      "EXTRA_MASTERD_ARGS": masterd_args,
+      }
+
+    result = utils.RunCmd([constants.DAEMON_UTIL, "start-master"], env=env)
+    if result.failed:
+      msg = "Can't start Ganeti master: %s" % result.output
+      logging.error(msg)
+      err_msgs.append(msg)
 
   if err_msgs:
     _Fail("; ".join(err_msgs))
 
   if err_msgs:
     _Fail("; ".join(err_msgs))
@@ -301,9 +316,11 @@ def StopMaster(stop_daemons):
     # but otherwise ignore the failure
 
   if stop_daemons:
     # but otherwise ignore the failure
 
   if stop_daemons:
-    # stop/kill the rapi and the master daemon
-    for daemon in constants.RAPI, constants.MASTERD:
-      utils.KillProcess(utils.ReadPidFile(utils.DaemonPidFileName(daemon)))
+    result = utils.RunCmd([constants.DAEMON_UTIL, "stop-master"])
+    if result.failed:
+      logging.error("Could not stop Ganeti master, command %s had exitcode %s"
+                    " and error %s",
+                    result.cmd, result.exit_code, result.output)
 
 
 def AddNode(dsa, dsapub, rsa, rsapub, sshkey, sshpub):
 
 
 def AddNode(dsa, dsapub, rsa, rsapub, sshkey, sshpub):
@@ -348,10 +365,13 @@ def AddNode(dsa, dsapub, rsa, rsapub, sshkey, sshpub):
 
   utils.AddAuthorizedKey(auth_keys, sshpub)
 
 
   utils.AddAuthorizedKey(auth_keys, sshpub)
 
-  utils.RunCmd([constants.SSH_INITD_SCRIPT, "restart"])
+  result = utils.RunCmd([constants.DAEMON_UTIL, "reload-ssh-keys"])
+  if result.failed:
+    _Fail("Unable to reload SSH keys (command %r, exit code %s, output %r)",
+          result.cmd, result.exit_code, result.output)
 
 
 
 
-def LeaveCluster():
+def LeaveCluster(modify_ssh_setup):
   """Cleans up and remove the current node.
 
   This function cleans up and prepares the current node to be removed
   """Cleans up and remove the current node.
 
   This function cleans up and prepares the current node to be removed
@@ -361,31 +381,34 @@ def LeaveCluster():
   L{errors.QuitGanetiException} which is used as a special case to
   shutdown the node daemon.
 
   L{errors.QuitGanetiException} which is used as a special case to
   shutdown the node daemon.
 
+  @param modify_ssh_setup: boolean
+
   """
   _CleanDirectory(constants.DATA_DIR)
   JobQueuePurge()
 
   """
   _CleanDirectory(constants.DATA_DIR)
   JobQueuePurge()
 
-  try:
-    priv_key, pub_key, auth_keys = ssh.GetUserFiles(constants.GANETI_RUNAS)
+  if modify_ssh_setup:
+    try:
+      priv_key, pub_key, auth_keys = ssh.GetUserFiles(constants.GANETI_RUNAS)
 
 
-    utils.RemoveAuthorizedKey(auth_keys, utils.ReadFile(pub_key))
+      utils.RemoveAuthorizedKey(auth_keys, utils.ReadFile(pub_key))
 
 
-    utils.RemoveFile(priv_key)
-    utils.RemoveFile(pub_key)
-  except errors.OpExecError:
-    logging.exception("Error while processing ssh files")
+      utils.RemoveFile(priv_key)
+      utils.RemoveFile(pub_key)
+    except errors.OpExecError:
+      logging.exception("Error while processing ssh files")
 
   try:
     utils.RemoveFile(constants.HMAC_CLUSTER_KEY)
     utils.RemoveFile(constants.RAPI_CERT_FILE)
     utils.RemoveFile(constants.SSL_CERT_FILE)
 
   try:
     utils.RemoveFile(constants.HMAC_CLUSTER_KEY)
     utils.RemoveFile(constants.RAPI_CERT_FILE)
     utils.RemoveFile(constants.SSL_CERT_FILE)
-  except:
+  except: # pylint: disable-msg=W0702
     logging.exception("Error while removing cluster secrets")
 
     logging.exception("Error while removing cluster secrets")
 
-  confd_pid = utils.ReadPidFile(utils.DaemonPidFileName(constants.CONFD))
-
-  if confd_pid:
-    utils.KillProcess(confd_pid, timeout=2)
+  result = utils.RunCmd([constants.DAEMON_UTIL, "stop", constants.CONFD])
+  if result.failed:
+    logging.error("Command %s failed with exitcode %s and error %s",
+                  result.cmd, result.exit_code, result.output)
 
   # Raise a custom exception (handled in ganeti-noded)
   raise errors.QuitGanetiException(True, 'Shutdown scheduled')
 
   # Raise a custom exception (handled in ganeti-noded)
   raise errors.QuitGanetiException(True, 'Shutdown scheduled')
@@ -506,6 +529,11 @@ def VerifyNode(what, cluster_name):
   if constants.NV_VGLIST in what:
     result[constants.NV_VGLIST] = utils.ListVolumeGroups()
 
   if constants.NV_VGLIST in what:
     result[constants.NV_VGLIST] = utils.ListVolumeGroups()
 
+  if constants.NV_PVLIST in what:
+    result[constants.NV_PVLIST] = \
+      bdev.LogicalVolume.GetPVInfo(what[constants.NV_PVLIST],
+                                   filter_allocatable=False)
+
   if constants.NV_VERSION in what:
     result[constants.NV_VERSION] = (constants.PROTOCOL_VERSION,
                                     constants.RELEASE_VERSION)
   if constants.NV_VERSION in what:
     result[constants.NV_VERSION] = (constants.PROTOCOL_VERSION,
                                     constants.RELEASE_VERSION)
@@ -533,6 +561,10 @@ def VerifyNode(what, cluster_name):
       tmpr.append("The procfs filesystem doesn't seem to be mounted"
                   " under /proc, missing required directory /proc/sys and"
                   " the file /proc/sysrq-trigger")
       tmpr.append("The procfs filesystem doesn't seem to be mounted"
                   " under /proc, missing required directory /proc/sys and"
                   " the file /proc/sysrq-trigger")
+
+  if constants.NV_TIME in what:
+    result[constants.NV_TIME] = utils.SplitTime(time.time())
+
   return result
 
 
   return result
 
 
@@ -648,7 +680,7 @@ def BridgesExist(bridges_list):
       missing.append(bridge)
 
   if missing:
       missing.append(bridge)
 
   if missing:
-    _Fail("Missing bridges %s", ", ".join(missing))
+    _Fail("Missing bridges %s", utils.CommaJoin(missing))
 
 
 def GetInstanceList(hypervisor_list):
 
 
 def GetInstanceList(hypervisor_list):
@@ -767,24 +799,43 @@ def GetAllInstancesInfo(hypervisor_list):
   return output
 
 
   return output
 
 
-def InstanceOsAdd(instance, reinstall):
+def _InstanceLogName(kind, os_name, instance):
+  """Compute the OS log filename for a given instance and operation.
+
+  The instance name and os name are passed in as strings since not all
+  operations have these as part of an instance object.
+
+  @type kind: string
+  @param kind: the operation type (e.g. add, import, etc.)
+  @type os_name: string
+  @param os_name: the os name
+  @type instance: string
+  @param instance: the name of the instance being imported/added/etc.
+
+  """
+  base = "%s-%s-%s-%d.log" % (kind, os_name, instance, int(time.time()))
+  return utils.PathJoin(constants.LOG_OS_DIR, base)
+
+
+def InstanceOsAdd(instance, reinstall, debug):
   """Add an OS to an instance.
 
   @type instance: L{objects.Instance}
   @param instance: Instance whose OS is to be installed
   @type reinstall: boolean
   @param reinstall: whether this is an instance reinstall
   """Add an OS to an instance.
 
   @type instance: L{objects.Instance}
   @param instance: Instance whose OS is to be installed
   @type reinstall: boolean
   @param reinstall: whether this is an instance reinstall
+  @type debug: integer
+  @param debug: debug level, passed to the OS scripts
   @rtype: None
 
   """
   inst_os = OSFromDisk(instance.os)
 
   @rtype: None
 
   """
   inst_os = OSFromDisk(instance.os)
 
-  create_env = OSEnvironment(instance, inst_os)
+  create_env = OSEnvironment(instance, inst_os, debug)
   if reinstall:
     create_env['INSTANCE_REINSTALL'] = "1"
 
   if reinstall:
     create_env['INSTANCE_REINSTALL'] = "1"
 
-  logfile = "%s/add-%s-%s-%d.log" % (constants.LOG_OS_DIR, instance.os,
-                                     instance.name, int(time.time()))
+  logfile = _InstanceLogName("add", instance.os, instance.name)
 
   result = utils.RunCmd([inst_os.create_script], env=create_env,
                         cwd=inst_os.path, output=logfile,)
 
   result = utils.RunCmd([inst_os.create_script], env=create_env,
                         cwd=inst_os.path, output=logfile,)
@@ -798,25 +849,26 @@ def InstanceOsAdd(instance, reinstall):
           " log file:\n%s", result.fail_reason, "\n".join(lines), log=False)
 
 
           " log file:\n%s", result.fail_reason, "\n".join(lines), log=False)
 
 
-def RunRenameInstance(instance, old_name):
+def RunRenameInstance(instance, old_name, debug):
   """Run the OS rename script for an instance.
 
   @type instance: L{objects.Instance}
   @param instance: Instance whose OS is to be installed
   @type old_name: string
   @param old_name: previous instance name
   """Run the OS rename script for an instance.
 
   @type instance: L{objects.Instance}
   @param instance: Instance whose OS is to be installed
   @type old_name: string
   @param old_name: previous instance name
+  @type debug: integer
+  @param debug: debug level, passed to the OS scripts
   @rtype: boolean
   @return: the success of the operation
 
   """
   inst_os = OSFromDisk(instance.os)
 
   @rtype: boolean
   @return: the success of the operation
 
   """
   inst_os = OSFromDisk(instance.os)
 
-  rename_env = OSEnvironment(instance, inst_os)
+  rename_env = OSEnvironment(instance, inst_os, debug)
   rename_env['OLD_INSTANCE_NAME'] = old_name
 
   rename_env['OLD_INSTANCE_NAME'] = old_name
 
-  logfile = "%s/rename-%s-%s-%s-%d.log" % (constants.LOG_OS_DIR, instance.os,
-                                           old_name,
-                                           instance.name, int(time.time()))
+  logfile = _InstanceLogName("rename", instance.os,
+                             "%s-%s" % (old_name, instance.name))
 
   result = utils.RunCmd([inst_os.rename_script], env=rename_env,
                         cwd=inst_os.path, output=logfile)
 
   result = utils.RunCmd([inst_os.rename_script], env=rename_env,
                         cwd=inst_os.path, output=logfile)
@@ -862,7 +914,7 @@ def _GetVGInfo(vg_name):
         "vg_free": int(round(float(valarr[1]), 0)),
         "pv_count": int(valarr[2]),
         }
         "vg_free": int(round(float(valarr[1]), 0)),
         "pv_count": int(valarr[2]),
         }
-    except ValueError, err:
+    except (TypeError, ValueError), err:
       logging.exception("Fail to parse vgs output: %s", err)
   else:
     logging.error("vgs output has the wrong number of fields (expected"
       logging.exception("Fail to parse vgs output: %s", err)
   else:
     logging.error("vgs output has the wrong number of fields (expected"
@@ -871,8 +923,8 @@ def _GetVGInfo(vg_name):
 
 
 def _GetBlockDevSymlinkPath(instance_name, idx):
 
 
 def _GetBlockDevSymlinkPath(instance_name, idx):
-  return os.path.join(constants.DISK_LINKS_DIR,
-                      "%s:%d" % (instance_name, idx))
+  return utils.PathJoin(constants.DISK_LINKS_DIR,
+                        "%s:%d" % (instance_name, idx))
 
 
 def _SymlinkBlockDev(instance_name, device_path, idx):
 
 
 def _SymlinkBlockDev(instance_name, device_path, idx):
@@ -985,42 +1037,51 @@ def InstanceShutdown(instance, timeout):
   """
   hv_name = instance.hypervisor
   hyper = hypervisor.GetHypervisor(hv_name)
   """
   hv_name = instance.hypervisor
   hyper = hypervisor.GetHypervisor(hv_name)
-  running_instances = hyper.ListInstances()
   iname = instance.name
 
   iname = instance.name
 
-  if iname not in running_instances:
+  if instance.name not in hyper.ListInstances():
     logging.info("Instance %s not running, doing nothing", iname)
     return
 
     logging.info("Instance %s not running, doing nothing", iname)
     return
 
-  start = time.time()
-  end = start + timeout
-  sleep_time = 1
+  class _TryShutdown:
+    def __init__(self):
+      self.tried_once = False
 
 
-  tried_once = False
-  while not tried_once and time.time() < end:
-    try:
-      hyper.StopInstance(instance, retry=tried_once)
-    except errors.HypervisorError, err:
-      _Fail("Failed to stop instance %s: %s", iname, err)
-    tried_once = True
-    time.sleep(sleep_time)
-    if instance.name not in hyper.ListInstances():
-      break
-    if sleep_time < 5:
-      # 1.2 behaves particularly good for our case:
-      # it gives us 10 increasing steps and caps just slightly above 5 seconds
-      sleep_time *= 1.2
-  else:
+    def __call__(self):
+      if iname not in hyper.ListInstances():
+        return
+
+      try:
+        hyper.StopInstance(instance, retry=self.tried_once)
+      except errors.HypervisorError, err:
+        if iname not in hyper.ListInstances():
+          # if the instance is no longer existing, consider this a
+          # success and go to cleanup
+          return
+
+        _Fail("Failed to stop instance %s: %s", iname, err)
+
+      self.tried_once = True
+
+      raise utils.RetryAgain()
+
+  try:
+    utils.Retry(_TryShutdown(), 5, timeout)
+  except utils.RetryTimeout:
     # the shutdown did not succeed
     logging.error("Shutdown of '%s' unsuccessful, forcing", iname)
 
     try:
       hyper.StopInstance(instance, force=True)
     except errors.HypervisorError, err:
     # the shutdown did not succeed
     logging.error("Shutdown of '%s' unsuccessful, forcing", iname)
 
     try:
       hyper.StopInstance(instance, force=True)
     except errors.HypervisorError, err:
-      _Fail("Failed to force stop instance %s: %s", iname, err)
+      if iname in hyper.ListInstances():
+        # only raise an error if the instance still exists, otherwise
+        # the error could simply be "instance ... unknown"!
+        _Fail("Failed to force stop instance %s: %s", iname, err)
 
     time.sleep(1)
 
     time.sleep(1)
-    if instance.name in GetInstanceList([hv_name]):
+
+    if iname in hyper.ListInstances():
       _Fail("Could not shutdown instance %s even by destroy", iname)
 
   _RemoveBlockDevLinks(iname, instance.disks)
       _Fail("Could not shutdown instance %s even by destroy", iname)
 
   _RemoveBlockDevLinks(iname, instance.disks)
@@ -1042,8 +1103,8 @@ def InstanceReboot(instance, reboot_type, shutdown_timeout):
         not accepted here, since that mode is handled differently, in
         cmdlib, and translates into full stop and start of the
         instance (instead of a call_instance_reboot RPC)
         not accepted here, since that mode is handled differently, in
         cmdlib, and translates into full stop and start of the
         instance (instead of a call_instance_reboot RPC)
-  @type timeout: integer
-  @param timeout: maximum timeout for soft shutdown
+  @type shutdown_timeout: integer
+  @param shutdown_timeout: maximum timeout for soft shutdown
   @rtype: None
 
   """
   @rtype: None
 
   """
@@ -1138,7 +1199,7 @@ def MigrateInstance(instance, target, live):
   hyper = hypervisor.GetHypervisor(instance.hypervisor)
 
   try:
   hyper = hypervisor.GetHypervisor(instance.hypervisor)
 
   try:
-    hyper.MigrateInstance(instance.name, target, live)
+    hyper.MigrateInstance(instance, target, live)
   except errors.HypervisorError, err:
     _Fail("Failed to migrate instance: %s", err, exc=True)
 
   except errors.HypervisorError, err:
     _Fail("Failed to migrate instance: %s", err, exc=True)
 
@@ -1164,6 +1225,8 @@ def BlockdevCreate(disk, size, owner, on_primary, info):
       it's not required to return anything.
 
   """
       it's not required to return anything.
 
   """
+  # TODO: remove the obsolete 'size' argument
+  # pylint: disable-msg=W0613
   clist = []
   if disk.children:
     for child in disk.children:
   clist = []
   if disk.children:
     for child in disk.children:
@@ -1175,6 +1238,7 @@ def BlockdevCreate(disk, size, owner, on_primary, info):
         # we need the children open in case the device itself has to
         # be assembled
         try:
         # we need the children open in case the device itself has to
         # be assembled
         try:
+          # pylint: disable-msg=E1103
           crdev.Open()
         except errors.BlockDeviceError, err:
           _Fail("Can't make child '%s' read-write: %s", child, err)
           crdev.Open()
         except errors.BlockDeviceError, err:
           _Fail("Can't make child '%s' read-write: %s", child, err)
@@ -1309,6 +1373,7 @@ def BlockdevAssemble(disk, owner, as_primary):
   try:
     result = _RecursiveAssembleBD(disk, owner, as_primary)
     if isinstance(result, bdev.BlockDev):
   try:
     result = _RecursiveAssembleBD(disk, owner, as_primary)
     if isinstance(result, bdev.BlockDev):
+      # pylint: disable-msg=E1103
       result = result.dev_path
   except errors.BlockDeviceError, err:
     _Fail("Error while assembling disk: %s", err, exc=True)
       result = result.dev_path
   except errors.BlockDeviceError, err:
     _Fail("Error while assembling disk: %s", err, exc=True)
@@ -1396,6 +1461,8 @@ def BlockdevRemovechildren(parent_cdev, new_cdevs):
       else:
         devs.append(bd.dev_path)
     else:
       else:
         devs.append(bd.dev_path)
     else:
+      if not utils.IsNormAbsPath(rpath):
+        _Fail("Strange path returned from StaticDevPath: '%s'", rpath)
       devs.append(rpath)
   parent_bdev.RemoveChildren(devs)
 
       devs.append(rpath)
   parent_bdev.RemoveChildren(devs)
 
@@ -1483,7 +1550,7 @@ def BlockdevGetsize(disks):
   for cf in disks:
     try:
       rbd = _RecursiveFindBD(cf)
   for cf in disks:
     try:
       rbd = _RecursiveFindBD(cf)
-    except errors.BlockDeviceError, err:
+    except errors.BlockDeviceError:
       result.append(None)
       continue
     if rbd is None:
       result.append(None)
       continue
     if rbd is None:
@@ -1604,22 +1671,20 @@ def _ErrnoOrStr(err):
   return detail
 
 
   return detail
 
 
-def _OSOndiskAPIVersion(name, os_dir):
+def _OSOndiskAPIVersion(os_dir):
   """Compute and return the API version of a given OS.
 
   """Compute and return the API version of a given OS.
 
-  This function will try to read the API version of the OS given by
-  the 'name' parameter and residing in the 'os_dir' directory.
+  This function will try to read the API version of the OS residing in
+  the 'os_dir' directory.
 
 
-  @type name: str
-  @param name: the OS name we should look for
   @type os_dir: str
   @type os_dir: str
-  @param os_dir: the directory inwhich we should look for the OS
+  @param os_dir: the directory in which we should look for the OS
   @rtype: tuple
   @return: tuple (status, data) with status denoting the validity and
       data holding either the vaid versions or an error message
 
   """
   @rtype: tuple
   @return: tuple (status, data) with status denoting the validity and
       data holding either the vaid versions or an error message
 
   """
-  api_file = os.path.sep.join([os_dir, constants.OS_API_FILE])
+  api_file = utils.PathJoin(os_dir, constants.OS_API_FILE)
 
   try:
     st = os.stat(api_file)
 
   try:
     st = os.stat(api_file)
@@ -1675,7 +1740,7 @@ def DiagnoseOS(top_dirs=None):
         logging.exception("Can't list the OS directory %s: %s", dir_name, err)
         break
       for name in f_names:
         logging.exception("Can't list the OS directory %s: %s", dir_name, err)
         break
       for name in f_names:
-        os_path = os.path.sep.join([dir_name, name])
+        os_path = utils.PathJoin(dir_name, name)
         status, os_inst = _TryOSFromDisk(name, base_dir=dir_name)
         if status:
           diagnose = ""
         status, os_inst = _TryOSFromDisk(name, base_dir=dir_name)
         if status:
           diagnose = ""
@@ -1704,12 +1769,13 @@ def _TryOSFromDisk(name, base_dir=None):
   """
   if base_dir is None:
     os_dir = utils.FindFile(name, constants.OS_SEARCH_PATH, os.path.isdir)
   """
   if base_dir is None:
     os_dir = utils.FindFile(name, constants.OS_SEARCH_PATH, os.path.isdir)
-    if os_dir is None:
-      return False, "Directory for OS %s not found in search path" % name
   else:
   else:
-    os_dir = os.path.sep.join([base_dir, name])
+    os_dir = utils.FindFile(name, [base_dir], os.path.isdir)
 
 
-  status, api_versions = _OSOndiskAPIVersion(name, os_dir)
+  if os_dir is None:
+    return False, "Directory for OS %s not found in search path" % name
+
+  status, api_versions = _OSOndiskAPIVersion(os_dir)
   if not status:
     # push the error up
     return status, api_versions
   if not status:
     # push the error up
     return status, api_versions
@@ -1724,23 +1790,23 @@ def _TryOSFromDisk(name, base_dir=None):
   if max(api_versions) >= constants.OS_API_V15:
     os_files[constants.OS_VARIANTS_FILE] = ''
 
   if max(api_versions) >= constants.OS_API_V15:
     os_files[constants.OS_VARIANTS_FILE] = ''
 
-  for name in os_files:
-    os_files[name] = os.path.sep.join([os_dir, name])
+  for filename in os_files:
+    os_files[filename] = utils.PathJoin(os_dir, filename)
 
     try:
 
     try:
-      st = os.stat(os_files[name])
+      st = os.stat(os_files[filename])
     except EnvironmentError, err:
       return False, ("File '%s' under path '%s' is missing (%s)" %
     except EnvironmentError, err:
       return False, ("File '%s' under path '%s' is missing (%s)" %
-                     (name, os_dir, _ErrnoOrStr(err)))
+                     (filename, os_dir, _ErrnoOrStr(err)))
 
     if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
       return False, ("File '%s' under path '%s' is not a regular file" %
 
     if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
       return False, ("File '%s' under path '%s' is not a regular file" %
-                     (name, os_dir))
+                     (filename, os_dir))
 
 
-    if name in constants.OS_SCRIPTS:
+    if filename in constants.OS_SCRIPTS:
       if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
         return False, ("File '%s' under path '%s' is not executable" %
       if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
         return False, ("File '%s' under path '%s' is not executable" %
-                       (name, os_dir))
+                       (filename, os_dir))
 
   variants = None
   if constants.OS_VARIANTS_FILE in os_files:
 
   variants = None
   if constants.OS_VARIANTS_FILE in os_files:
@@ -1790,13 +1856,13 @@ def OSFromDisk(name, base_dir=None):
   return payload
 
 
   return payload
 
 
-def OSEnvironment(instance, os, debug=0):
+def OSEnvironment(instance, inst_os, debug=0):
   """Calculate the environment for an os script.
 
   @type instance: L{objects.Instance}
   @param instance: target instance for the os script run
   """Calculate the environment for an os script.
 
   @type instance: L{objects.Instance}
   @param instance: target instance for the os script run
-  @type os: L{objects.OS}
-  @param os: operating system for which the environment is being built
+  @type inst_os: L{objects.OS}
+  @param inst_os: operating system for which the environment is being built
   @type debug: integer
   @param debug: debug level (0 or 1, for OS Api 10)
   @rtype: dict
   @type debug: integer
   @param debug: debug level (0 or 1, for OS Api 10)
   @rtype: dict
@@ -1806,7 +1872,8 @@ def OSEnvironment(instance, os, debug=0):
 
   """
   result = {}
 
   """
   result = {}
-  api_version = max(constants.OS_API_VERSIONS.intersection(os.api_versions))
+  api_version = \
+    max(constants.OS_API_VERSIONS.intersection(inst_os.api_versions))
   result['OS_API_VERSION'] = '%d' % api_version
   result['INSTANCE_NAME'] = instance.name
   result['INSTANCE_OS'] = instance.os
   result['OS_API_VERSION'] = '%d' % api_version
   result['INSTANCE_NAME'] = instance.name
   result['INSTANCE_OS'] = instance.os
@@ -1818,7 +1885,7 @@ def OSEnvironment(instance, os, debug=0):
     try:
       variant = instance.os.split('+', 1)[1]
     except IndexError:
     try:
       variant = instance.os.split('+', 1)[1]
     except IndexError:
-      variant = os.supported_variants[0]
+      variant = inst_os.supported_variants[0]
     result['OS_VARIANT'] = variant
   for idx, disk in enumerate(instance.disks):
     real_disk = _RecursiveFindBD(disk)
     result['OS_VARIANT'] = variant
   for idx, disk in enumerate(instance.disks):
     real_disk = _RecursiveFindBD(disk)
@@ -1891,19 +1958,15 @@ def BlockdevSnapshot(disk):
   @return: snapshot disk path
 
   """
   @return: snapshot disk path
 
   """
-  if disk.children:
-    if len(disk.children) == 1:
-      # only one child, let's recurse on it
-      return BlockdevSnapshot(disk.children[0])
-    else:
-      # more than one child, choose one that matches
-      for child in disk.children:
-        if child.size == disk.size:
-          # return implies breaking the loop
-          return BlockdevSnapshot(child)
+  if disk.dev_type == constants.LD_DRBD8:
+    if not disk.children:
+      _Fail("DRBD device '%s' without backing storage cannot be snapshotted",
+            disk.unique_id)
+    return BlockdevSnapshot(disk.children[0])
   elif disk.dev_type == constants.LD_LV:
     r_dev = _RecursiveFindBD(disk)
     if r_dev is not None:
   elif disk.dev_type == constants.LD_LV:
     r_dev = _RecursiveFindBD(disk)
     if r_dev is not None:
+      # FIXME: choose a saner value for the snapshot size
       # let's stay on the safe side and ask for the full size, for now
       return r_dev.Snapshot(disk.size)
     else:
       # let's stay on the safe side and ask for the full size, for now
       return r_dev.Snapshot(disk.size)
     else:
@@ -1913,7 +1976,7 @@ def BlockdevSnapshot(disk):
           disk.unique_id, disk.dev_type)
 
 
           disk.unique_id, disk.dev_type)
 
 
-def ExportSnapshot(disk, dest_node, instance, cluster_name, idx):
+def ExportSnapshot(disk, dest_node, instance, cluster_name, idx, debug):
   """Export a block device snapshot to a remote node.
 
   @type disk: L{objects.Disk}
   """Export a block device snapshot to a remote node.
 
   @type disk: L{objects.Disk}
@@ -1927,16 +1990,17 @@ def ExportSnapshot(disk, dest_node, instance, cluster_name, idx):
   @type idx: int
   @param idx: the index of the disk in the instance's disk list,
       used to export to the OS scripts environment
   @type idx: int
   @param idx: the index of the disk in the instance's disk list,
       used to export to the OS scripts environment
+  @type debug: integer
+  @param debug: debug level, passed to the OS scripts
   @rtype: None
 
   """
   inst_os = OSFromDisk(instance.os)
   @rtype: None
 
   """
   inst_os = OSFromDisk(instance.os)
-  export_env = OSEnvironment(instance, inst_os)
+  export_env = OSEnvironment(instance, inst_os, debug)
 
   export_script = inst_os.export_script
 
 
   export_script = inst_os.export_script
 
-  logfile = "%s/exp-%s-%s-%s.log" % (constants.LOG_OS_DIR, inst_os.name,
-                                     instance.name, int(time.time()))
+  logfile = _InstanceLogName("export", inst_os.name, instance.name)
   if not os.path.exists(constants.LOG_OS_DIR):
     os.mkdir(constants.LOG_OS_DIR, 0750)
   real_disk = _RecursiveFindBD(disk)
   if not os.path.exists(constants.LOG_OS_DIR):
     os.mkdir(constants.LOG_OS_DIR, 0750)
   real_disk = _RecursiveFindBD(disk)
@@ -1948,7 +2012,7 @@ def ExportSnapshot(disk, dest_node, instance, cluster_name, idx):
   export_env['EXPORT_DEVICE'] = real_disk.dev_path
   export_env['EXPORT_INDEX'] = str(idx)
 
   export_env['EXPORT_DEVICE'] = real_disk.dev_path
   export_env['EXPORT_INDEX'] = str(idx)
 
-  destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
+  destdir = utils.PathJoin(constants.EXPORT_DIR, instance.name + ".new")
   destfile = disk.physical_id[1]
 
   # the target command is built out of three individual commands,
   destfile = disk.physical_id[1]
 
   # the target command is built out of three individual commands,
@@ -1959,8 +2023,8 @@ def ExportSnapshot(disk, dest_node, instance, cluster_name, idx):
 
   comprcmd = "gzip"
 
 
   comprcmd = "gzip"
 
-  destcmd = utils.BuildShellCmd("mkdir -p %s && cat > %s/%s",
-                                destdir, destdir, destfile)
+  destcmd = utils.BuildShellCmd("mkdir -p %s && cat > %s",
+                                destdir, utils.PathJoin(destdir, destfile))
   remotecmd = _GetSshRunner(cluster_name).BuildCmd(dest_node,
                                                    constants.GANETI_RUNAS,
                                                    destcmd)
   remotecmd = _GetSshRunner(cluster_name).BuildCmd(dest_node,
                                                    constants.GANETI_RUNAS,
                                                    destcmd)
@@ -1988,8 +2052,8 @@ def FinalizeExport(instance, snap_disks):
   @rtype: None
 
   """
   @rtype: None
 
   """
-  destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
-  finaldestdir = os.path.join(constants.EXPORT_DIR, instance.name)
+  destdir = utils.PathJoin(constants.EXPORT_DIR, instance.name + ".new")
+  finaldestdir = utils.PathJoin(constants.EXPORT_DIR, instance.name)
 
   config = objects.SerializableConfigParser()
 
 
   config = objects.SerializableConfigParser()
 
@@ -2032,7 +2096,7 @@ def FinalizeExport(instance, snap_disks):
 
   config.set(constants.INISECT_INS, 'disk_count' , '%d' % disk_total)
 
 
   config.set(constants.INISECT_INS, 'disk_count' , '%d' % disk_total)
 
-  utils.WriteFile(os.path.join(destdir, constants.EXPORT_CONF_FILE),
+  utils.WriteFile(utils.PathJoin(destdir, constants.EXPORT_CONF_FILE),
                   data=config.Dumps())
   shutil.rmtree(finaldestdir, True)
   shutil.move(destdir, finaldestdir)
                   data=config.Dumps())
   shutil.rmtree(finaldestdir, True)
   shutil.move(destdir, finaldestdir)
@@ -2049,7 +2113,7 @@ def ExportInfo(dest):
       export info
 
   """
       export info
 
   """
-  cff = os.path.join(dest, constants.EXPORT_CONF_FILE)
+  cff = utils.PathJoin(dest, constants.EXPORT_CONF_FILE)
 
   config = objects.SerializableConfigParser()
   config.read(cff)
 
   config = objects.SerializableConfigParser()
   config.read(cff)
@@ -2061,7 +2125,7 @@ def ExportInfo(dest):
   return config.Dumps()
 
 
   return config.Dumps()
 
 
-def ImportOSIntoInstance(instance, src_node, src_images, cluster_name):
+def ImportOSIntoInstance(instance, src_node, src_images, cluster_name, debug):
   """Import an os image into an instance.
 
   @type instance: L{objects.Instance}
   """Import an os image into an instance.
 
   @type instance: L{objects.Instance}
@@ -2070,16 +2134,17 @@ def ImportOSIntoInstance(instance, src_node, src_images, cluster_name):
   @param src_node: source node for the disk images
   @type src_images: list of string
   @param src_images: absolute paths of the disk images
   @param src_node: source node for the disk images
   @type src_images: list of string
   @param src_images: absolute paths of the disk images
+  @type debug: integer
+  @param debug: debug level, passed to the OS scripts
   @rtype: list of boolean
   @return: each boolean represent the success of importing the n-th disk
 
   """
   inst_os = OSFromDisk(instance.os)
   @rtype: list of boolean
   @return: each boolean represent the success of importing the n-th disk
 
   """
   inst_os = OSFromDisk(instance.os)
-  import_env = OSEnvironment(instance, inst_os)
+  import_env = OSEnvironment(instance, inst_os, debug)
   import_script = inst_os.import_script
 
   import_script = inst_os.import_script
 
-  logfile = "%s/import-%s-%s-%s.log" % (constants.LOG_OS_DIR, instance.os,
-                                        instance.name, int(time.time()))
+  logfile = _InstanceLogName("import", instance.os, instance.name)
   if not os.path.exists(constants.LOG_OS_DIR):
     os.mkdir(constants.LOG_OS_DIR, 0750)
 
   if not os.path.exists(constants.LOG_OS_DIR):
     os.mkdir(constants.LOG_OS_DIR, 0750)
 
@@ -2130,7 +2195,7 @@ def RemoveExport(export):
   @rtype: None
 
   """
   @rtype: None
 
   """
-  target = os.path.join(constants.EXPORT_DIR, export)
+  target = utils.PathJoin(constants.EXPORT_DIR, export)
 
   try:
     shutil.rmtree(target)
 
   try:
     shutil.rmtree(target)
@@ -2192,6 +2257,8 @@ def _TransformFileStorageDir(file_storage_dir):
   @return: the normalized path if valid, None otherwise
 
   """
   @return: the normalized path if valid, None otherwise
 
   """
+  if not constants.ENABLE_FILE_STORAGE:
+    _Fail("File storage disabled at configure time")
   cfg = _GetConfig()
   file_storage_dir = os.path.normpath(file_storage_dir)
   base_file_storage_dir = cfg.GetFileStorageDir()
   cfg = _GetConfig()
   file_storage_dir = os.path.normpath(file_storage_dir)
   base_file_storage_dir = cfg.GetFileStorageDir()
@@ -2417,15 +2484,18 @@ def DemoteFromMC():
   master, myself = ssconf.GetMasterAndMyself()
   if master == myself:
     _Fail("ssconf status shows I'm the master node, will not demote")
   master, myself = ssconf.GetMasterAndMyself()
   if master == myself:
     _Fail("ssconf status shows I'm the master node, will not demote")
-  pid_file = utils.DaemonPidFileName(constants.MASTERD)
-  if utils.IsProcessAlive(utils.ReadPidFile(pid_file)):
+
+  result = utils.RunCmd([constants.DAEMON_UTIL, "check", constants.MASTERD])
+  if not result.failed:
     _Fail("The master daemon is running, will not demote")
     _Fail("The master daemon is running, will not demote")
+
   try:
     if os.path.isfile(constants.CLUSTER_CONF_FILE):
       utils.CreateBackup(constants.CLUSTER_CONF_FILE)
   except EnvironmentError, err:
     if err.errno != errno.ENOENT:
       _Fail("Error while backing up cluster file: %s", err, exc=True)
   try:
     if os.path.isfile(constants.CLUSTER_CONF_FILE):
       utils.CreateBackup(constants.CLUSTER_CONF_FILE)
   except EnvironmentError, err:
     if err.errno != errno.ENOENT:
       _Fail("Error while backing up cluster file: %s", err, exc=True)
+
   utils.RemoveFile(constants.CLUSTER_CONF_FILE)
 
 
   utils.RemoveFile(constants.CLUSTER_CONF_FILE)
 
 
@@ -2482,20 +2552,22 @@ def DrbdAttachNet(nodes_ip, disks, instance_name, multimaster):
       rd.AttachNet(multimaster)
     except errors.BlockDeviceError, err:
       _Fail("Can't change network configuration: %s", err)
       rd.AttachNet(multimaster)
     except errors.BlockDeviceError, err:
       _Fail("Can't change network configuration: %s", err)
+
   # wait until the disks are connected; we need to retry the re-attach
   # if the device becomes standalone, as this might happen if the one
   # node disconnects and reconnects in a different mode before the
   # other node reconnects; in this case, one or both of the nodes will
   # decide it has wrong configuration and switch to standalone
   # wait until the disks are connected; we need to retry the re-attach
   # if the device becomes standalone, as this might happen if the one
   # node disconnects and reconnects in a different mode before the
   # other node reconnects; in this case, one or both of the nodes will
   # decide it has wrong configuration and switch to standalone
-  RECONNECT_TIMEOUT = 2 * 60
-  sleep_time = 0.100 # start with 100 miliseconds
-  timeout_limit = time.time() + RECONNECT_TIMEOUT
-  while time.time() < timeout_limit:
+
+  def _Attach():
     all_connected = True
     all_connected = True
+
     for rd in bdevs:
       stats = rd.GetProcStatus()
     for rd in bdevs:
       stats = rd.GetProcStatus()
-      if not (stats.is_connected or stats.is_in_resync):
-        all_connected = False
+
+      all_connected = (all_connected and
+                       (stats.is_connected or stats.is_in_resync))
+
       if stats.is_standalone:
         # peer had different config info and this node became
         # standalone, even though this should not happen with the
       if stats.is_standalone:
         # peer had different config info and this node became
         # standalone, even though this should not happen with the
@@ -2504,12 +2576,16 @@ def DrbdAttachNet(nodes_ip, disks, instance_name, multimaster):
           rd.AttachNet(multimaster)
         except errors.BlockDeviceError, err:
           _Fail("Can't change network configuration: %s", err)
           rd.AttachNet(multimaster)
         except errors.BlockDeviceError, err:
           _Fail("Can't change network configuration: %s", err)
-    if all_connected:
-      break
-    time.sleep(sleep_time)
-    sleep_time = min(5, sleep_time * 1.5)
-  if not all_connected:
+
+    if not all_connected:
+      raise utils.RetryAgain()
+
+  try:
+    # Start with a delay of 100 miliseconds and go up to 5 seconds
+    utils.Retry(_Attach, (0.1, 1.5, 5.0), 2 * 60)
+  except utils.RetryTimeout:
     _Fail("Timeout in disk reconnecting")
     _Fail("Timeout in disk reconnecting")
+
   if multimaster:
     # change to primary mode
     for rd in bdevs:
   if multimaster:
     # change to primary mode
     for rd in bdevs:
@@ -2523,14 +2599,25 @@ def DrbdWaitSync(nodes_ip, disks):
   """Wait until DRBDs have synchronized.
 
   """
   """Wait until DRBDs have synchronized.
 
   """
+  def _helper(rd):
+    stats = rd.GetProcStatus()
+    if not (stats.is_connected or stats.is_in_resync):
+      raise utils.RetryAgain()
+    return stats
+
   bdevs = _FindDisks(nodes_ip, disks)
 
   min_resync = 100
   alldone = True
   for rd in bdevs:
   bdevs = _FindDisks(nodes_ip, disks)
 
   min_resync = 100
   alldone = True
   for rd in bdevs:
-    stats = rd.GetProcStatus()
-    if not (stats.is_connected or stats.is_in_resync):
-      _Fail("DRBD device %s is not in sync: stats=%s", rd, stats)
+    try:
+      # poll each second for 15 seconds
+      stats = utils.Retry(_helper, 1, 15, args=[rd])
+    except utils.RetryTimeout:
+      stats = rd.GetProcStatus()
+      # last check
+      if not (stats.is_connected or stats.is_in_resync):
+        _Fail("DRBD device %s is not in sync: stats=%s", rd, stats)
     alldone = alldone and (not stats.is_in_resync)
     if stats.sync_percent is not None:
       min_resync = min(min_resync, stats.sync_percent)
     alldone = alldone and (not stats.is_in_resync)
     if stats.sync_percent is not None:
       min_resync = min(min_resync, stats.sync_percent)
@@ -2564,8 +2651,6 @@ class HooksRunner(object):
   on the master side.
 
   """
   on the master side.
 
   """
-  RE_MASK = re.compile("^[a-zA-Z0-9_-]+$")
-
   def __init__(self, hooks_base_dir=None):
     """Constructor for hooks runner.
 
   def __init__(self, hooks_base_dir=None):
     """Constructor for hooks runner.
 
@@ -2576,57 +2661,9 @@ class HooksRunner(object):
     """
     if hooks_base_dir is None:
       hooks_base_dir = constants.HOOKS_BASE_DIR
     """
     if hooks_base_dir is None:
       hooks_base_dir = constants.HOOKS_BASE_DIR
-    self._BASE_DIR = hooks_base_dir
-
-  @staticmethod
-  def ExecHook(script, env):
-    """Exec one hook script.
-
-    @type script: str
-    @param script: the full path to the script
-    @type env: dict
-    @param env: the environment with which to exec the script
-    @rtype: tuple (success, message)
-    @return: a tuple of success and message, where success
-        indicates the succes of the operation, and message
-        which will contain the error details in case we
-        failed
-
-    """
-    # exec the process using subprocess and log the output
-    fdstdin = None
-    try:
-      fdstdin = open("/dev/null", "r")
-      child = subprocess.Popen([script], stdin=fdstdin, stdout=subprocess.PIPE,
-                               stderr=subprocess.STDOUT, close_fds=True,
-                               shell=False, cwd="/", env=env)
-      output = ""
-      try:
-        output = child.stdout.read(4096)
-        child.stdout.close()
-      except EnvironmentError, err:
-        output += "Hook script error: %s" % str(err)
-
-      while True:
-        try:
-          result = child.wait()
-          break
-        except EnvironmentError, err:
-          if err.errno == errno.EINTR:
-            continue
-          raise
-    finally:
-      # try not to leak fds
-      for fd in (fdstdin, ):
-        if fd is not None:
-          try:
-            fd.close()
-          except EnvironmentError, err:
-            # just log the error
-            #logging.exception("Error while closing fd %s", fd)
-            pass
-
-    return result == 0, utils.SafeEncode(output.strip())
+    # yeah, _BASE_DIR is not valid for attributes, we use it like a
+    # constant
+    self._BASE_DIR = hooks_base_dir # pylint: disable-msg=C0103
 
   def RunHooks(self, hpath, phase, env):
     """Run the scripts in the hooks directory.
 
   def RunHooks(self, hpath, phase, env):
     """Run the scripts in the hooks directory.
@@ -2657,34 +2694,35 @@ class HooksRunner(object):
     else:
       _Fail("Unknown hooks phase '%s'", phase)
 
     else:
       _Fail("Unknown hooks phase '%s'", phase)
 
-    rr = []
 
     subdir = "%s-%s.d" % (hpath, suffix)
 
     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
-          self.RE_MASK.match(relname) is not None):
+    dir_name = utils.PathJoin(self._BASE_DIR, subdir)
+
+    results = []
+
+    if not os.path.isdir(dir_name):
+      # for non-existing/non-dirs, we simply exit instead of logging a
+      # warning at every operation
+      return results
+
+    runparts_results = utils.RunParts(dir_name, env=env, reset_env=True)
+
+    for (relname, relstatus, runresult)  in runparts_results:
+      if relstatus == constants.RUNPARTS_SKIP:
         rrval = constants.HKR_SKIP
         output = ""
         rrval = constants.HKR_SKIP
         output = ""
-      else:
-        result, output = self.ExecHook(fname, env)
-        if not result:
+      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
         else:
           rrval = constants.HKR_SUCCESS
           rrval = constants.HKR_FAIL
         else:
           rrval = constants.HKR_SUCCESS
-      rr.append(("%s/%s" % (subdir, relname), rrval, output))
+        output = utils.SafeEncode(runresult.output.strip())
+      results.append(("%s/%s" % (subdir, relname), rrval, output))
 
 
-    return rr
+    return results
 
 
 class IAllocatorRunner(object):
 
 
 class IAllocatorRunner(object):
@@ -2694,7 +2732,8 @@ class IAllocatorRunner(object):
   the master side.
 
   """
   the master side.
 
   """
-  def Run(self, name, idata):
+  @staticmethod
+  def Run(name, idata):
     """Run an iallocator script.
 
     @type name: str
     """Run an iallocator script.
 
     @type name: str
@@ -2750,7 +2789,7 @@ class DevCacheManager(object):
     if dev_path.startswith(cls._DEV_PREFIX):
       dev_path = dev_path[len(cls._DEV_PREFIX):]
     dev_path = dev_path.replace("/", "_")
     if dev_path.startswith(cls._DEV_PREFIX):
       dev_path = dev_path[len(cls._DEV_PREFIX):]
     dev_path = dev_path.replace("/", "_")
-    fpath = "%s/bdev_%s" % (cls._ROOT_DIR, dev_path)
+    fpath = utils.PathJoin(cls._ROOT_DIR, "bdev_%s" % dev_path)
     return fpath
 
   @classmethod
     return fpath
 
   @classmethod