Revert "jqueue: Resume jobs from “waitlock” status"
[ganeti-local] / lib / utils.py
index f8c7299..441e5bb 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007 Google Inc.
+# Copyright (C) 2006, 2007, 2010 Google Inc.
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -49,18 +49,19 @@ import datetime
 import calendar
 import hmac
 import collections
-import struct
-import IN
 
 from cStringIO import StringIO
 
 try:
-  from hashlib import sha1
+  # pylint: disable-msg=F0401
+  import ctypes
 except ImportError:
-  import sha as sha1
+  ctypes = None
 
 from ganeti import errors
 from ganeti import constants
+from ganeti import compat
+from ganeti import netutils
 
 
 _locksheld = []
@@ -80,22 +81,16 @@ X509_SIGNATURE = re.compile(r"^%s:\s*(?P<salt>%s+)/(?P<sign>%s+)$" %
                              HEX_CHAR_RE, HEX_CHAR_RE),
                             re.S | re.I)
 
-# Structure definition for getsockopt(SOL_SOCKET, SO_PEERCRED, ...):
-# struct ucred { pid_t pid; uid_t uid; gid_t gid; };
-#
-# The GNU C Library defines gid_t and uid_t to be "unsigned int" and
-# pid_t to "int".
-#
-# IEEE Std 1003.1-2008:
-# "nlink_t, uid_t, gid_t, and id_t shall be integer types"
-# "blksize_t, pid_t, and ssize_t shall be signed integer types"
-_STRUCT_UCRED = "iII"
-_STRUCT_UCRED_SIZE = struct.calcsize(_STRUCT_UCRED)
+_VALID_SERVICE_NAME_RE = re.compile("^[-_.a-zA-Z0-9]{1,128}$")
 
 # Certificate verification results
 (CERT_WARNING,
  CERT_ERROR) = range(1, 3)
 
+# Flags for mlockall() (from bits/mman.h)
+_MCL_CURRENT = 1
+_MCL_FUTURE = 2
+
 
 class RunResult(object):
   """Holds the result of running external programs.
@@ -564,7 +559,9 @@ def RetryOnSignal(fn, *args, **kwargs):
     except EnvironmentError, err:
       if err.errno != errno.EINTR:
         raise
-    except select.error, err:
+    except (socket.error, select.error), err:
+      # In python 2.6 and above select.error is an IOError, so it's handled
+      # above, in 2.5 and below it's not, and it's handled here.
       if not (err.args and err.args[0] == errno.EINTR):
         raise
 
@@ -606,19 +603,6 @@ def RunParts(dir_name, env=None, reset_env=False):
   return rr
 
 
-def GetSocketCredentials(sock):
-  """Returns the credentials of the foreign process connected to a socket.
-
-  @param sock: Unix socket
-  @rtype: tuple; (number, number, number)
-  @return: The PID, UID and GID of the connected foreign process.
-
-  """
-  peercred = sock.getsockopt(socket.SOL_SOCKET, IN.SO_PEERCRED,
-                             _STRUCT_UCRED_SIZE)
-  return struct.unpack(_STRUCT_UCRED, peercred)
-
-
 def RemoveFile(filename):
   """Remove a file ignoring some errors.
 
@@ -636,6 +620,24 @@ def RemoveFile(filename):
       raise
 
 
+def RemoveDir(dirname):
+  """Remove an empty directory.
+
+  Remove a directory, ignoring non-existing ones.
+  Other errors are passed. This includes the case,
+  where the directory is not empty, so it can't be removed.
+
+  @type dirname: str
+  @param dirname: the empty directory to be removed
+
+  """
+  try:
+    os.rmdir(dirname)
+  except OSError, err:
+    if err.errno != errno.ENOENT:
+      raise
+
+
 def RenameFile(old, new, mkdir=False, mkdir_mode=0750):
   """Renames a file.
 
@@ -721,10 +723,7 @@ def _FingerprintFile(filename):
 
   f = open(filename)
 
-  if callable(sha1):
-    fp = sha1()
-  else:
-    fp = sha1.new()
+  fp = compat.sha1_hash()
   while True:
     data = f.read(4096)
     if not data:
@@ -787,8 +786,10 @@ def ForceDictType(target, key_types, allowed_values=None):
       msg = "'%s' has non-enforceable type %s" % (key, ktype)
       raise errors.ProgrammerError(msg)
 
-    if ktype == constants.VTYPE_STRING:
-      if not isinstance(target[key], basestring):
+    if ktype in (constants.VTYPE_STRING, constants.VTYPE_MAYBE_STRING):
+      if target[key] is None and ktype == constants.VTYPE_MAYBE_STRING:
+        pass
+      elif not isinstance(target[key], basestring):
         if isinstance(target[key], bool) and not target[key]:
           target[key] = ''
         else:
@@ -822,6 +823,17 @@ def ForceDictType(target, key_types, allowed_values=None):
         raise errors.TypeEnforcementError(msg)
 
 
+def _GetProcStatusPath(pid):
+  """Returns the path for a PID's proc status file.
+
+  @type pid: int
+  @param pid: Process ID
+  @rtype: string
+
+  """
+  return "/proc/%d/status" % pid
+
+
 def IsProcessAlive(pid):
   """Check if a given pid exists on the system.
 
@@ -848,15 +860,99 @@ def IsProcessAlive(pid):
   if pid <= 0:
     return False
 
-  proc_entry = "/proc/%d/status" % pid
   # /proc in a multiprocessor environment can have strange behaviors.
   # Retry the os.stat a few times until we get a good result.
   try:
-    return Retry(_TryStat, (0.01, 1.5, 0.1), 0.5, args=[proc_entry])
+    return Retry(_TryStat, (0.01, 1.5, 0.1), 0.5,
+                 args=[_GetProcStatusPath(pid)])
   except RetryTimeout, err:
     err.RaiseInner()
 
 
+def _ParseSigsetT(sigset):
+  """Parse a rendered sigset_t value.
+
+  This is the opposite of the Linux kernel's fs/proc/array.c:render_sigset_t
+  function.
+
+  @type sigset: string
+  @param sigset: Rendered signal set from /proc/$pid/status
+  @rtype: set
+  @return: Set of all enabled signal numbers
+
+  """
+  result = set()
+
+  signum = 0
+  for ch in reversed(sigset):
+    chv = int(ch, 16)
+
+    # The following could be done in a loop, but it's easier to read and
+    # understand in the unrolled form
+    if chv & 1:
+      result.add(signum + 1)
+    if chv & 2:
+      result.add(signum + 2)
+    if chv & 4:
+      result.add(signum + 3)
+    if chv & 8:
+      result.add(signum + 4)
+
+    signum += 4
+
+  return result
+
+
+def _GetProcStatusField(pstatus, field):
+  """Retrieves a field from the contents of a proc status file.
+
+  @type pstatus: string
+  @param pstatus: Contents of /proc/$pid/status
+  @type field: string
+  @param field: Name of field whose value should be returned
+  @rtype: string
+
+  """
+  for line in pstatus.splitlines():
+    parts = line.split(":", 1)
+
+    if len(parts) < 2 or parts[0] != field:
+      continue
+
+    return parts[1].strip()
+
+  return None
+
+
+def IsProcessHandlingSignal(pid, signum, status_path=None):
+  """Checks whether a process is handling a signal.
+
+  @type pid: int
+  @param pid: Process ID
+  @type signum: int
+  @param signum: Signal number
+  @rtype: bool
+
+  """
+  if status_path is None:
+    status_path = _GetProcStatusPath(pid)
+
+  try:
+    proc_status = ReadFile(status_path)
+  except EnvironmentError, err:
+    # In at least one case, reading /proc/$pid/status failed with ESRCH.
+    if err.errno in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL, errno.ESRCH):
+      return False
+    raise
+
+  sigcgt = _GetProcStatusField(proc_status, "SigCgt")
+  if sigcgt is None:
+    raise RuntimeError("%s is missing 'SigCgt' field" % status_path)
+
+  # Now check whether signal is handled
+  return signum in _ParseSigsetT(sigcgt)
+
+
 def ReadPidFile(pidfile):
   """Read a pid from a file.
 
@@ -868,7 +964,7 @@ def ReadPidFile(pidfile):
 
   """
   try:
-    raw_data = ReadFile(pidfile)
+    raw_data = ReadOneLineFile(pidfile)
   except EnvironmentError, err:
     if err.errno != errno.ENOENT:
       logging.exception("Can't read pid file")
@@ -960,92 +1056,28 @@ def MatchNameComponent(key, name_list, case_sensitive=True):
   return None
 
 
-class HostInfo:
-  """Class implementing resolver and hostname functionality
-
-  """
-  _VALID_NAME_RE = re.compile("^[a-z0-9._-]{1,255}$")
-
-  def __init__(self, name=None):
-    """Initialize the host name object.
-
-    If the name argument is not passed, it will use this system's
-    name.
-
-    """
-    if name is None:
-      name = self.SysName()
-
-    self.query = name
-    self.name, self.aliases, self.ipaddrs = self.LookupHostname(name)
-    self.ip = self.ipaddrs[0]
-
-  def ShortName(self):
-    """Returns the hostname without domain.
-
-    """
-    return self.name.split('.')[0]
-
-  @staticmethod
-  def SysName():
-    """Return the current system's name.
-
-    This is simply a wrapper over C{socket.gethostname()}.
-
-    """
-    return socket.gethostname()
-
-  @staticmethod
-  def LookupHostname(hostname):
-    """Look up hostname
-
-    @type hostname: str
-    @param hostname: hostname to look up
-
-    @rtype: tuple
-    @return: a tuple (name, aliases, ipaddrs) as returned by
-        C{socket.gethostbyname_ex}
-    @raise errors.ResolverError: in case of errors in resolving
+def ValidateServiceName(name):
+  """Validate the given service name.
 
-    """
-    try:
-      result = socket.gethostbyname_ex(hostname)
-    except socket.gaierror, err:
-      # hostname not found in DNS
-      raise errors.ResolverError(hostname, err.args[0], err.args[1])
-
-    return result
-
-  @classmethod
-  def NormalizeName(cls, hostname):
-    """Validate and normalize the given hostname.
+  @type name: number or string
+  @param name: Service name or port specification
 
-    @attention: the validation is a bit more relaxed than the standards
-        require; most importantly, we allow underscores in names
-    @raise errors.OpPrereqError: when the name is not valid
+  """
+  try:
+    numport = int(name)
+  except (ValueError, TypeError):
+    # Non-numeric service name
+    valid = _VALID_SERVICE_NAME_RE.match(name)
+  else:
+    # Numeric port (protocols other than TCP or UDP might need adjustments
+    # here)
+    valid = (numport >= 0 and numport < (1 << 16))
 
-    """
-    hostname = hostname.lower()
-    if (not cls._VALID_NAME_RE.match(hostname) or
-        # double-dots, meaning empty label
-        ".." in hostname or
-        # empty initial label
-        hostname.startswith(".")):
-      raise errors.OpPrereqError("Invalid hostname '%s'" % hostname,
-                                 errors.ECODE_INVAL)
-    if hostname.endswith("."):
-      hostname = hostname.rstrip(".")
-    return hostname
-
-
-def GetHostInfo(name=None):
-  """Lookup host name and raise an OpPrereqError for failures"""
+  if not valid:
+    raise errors.OpPrereqError("Invalid service name '%s'" % name,
+                               errors.ECODE_INVAL)
 
-  try:
-    return HostInfo(name)
-  except errors.ResolverError, err:
-    raise errors.OpPrereqError("The given name (%s) does not resolve: %s" %
-                               (err[0], err[2]), errors.ECODE_RESOLVER)
+  return name
 
 
 def ListVolumeGroups():
@@ -1147,24 +1179,6 @@ def TryConvert(fn, val):
   return nv
 
 
-def IsValidIP(ip):
-  """Verifies the syntax of an IPv4 address.
-
-  This function checks if the IPv4 address passes is valid or not based
-  on syntax (not IP range, class calculations, etc.).
-
-  @type ip: str
-  @param ip: the address to be checked
-  @rtype: a regular expression match object
-  @return: a regular expression match object, or None if the
-      address is not valid
-
-  """
-  unit = "(0|[1-9]\d{0,2})"
-  #TODO: convert and return only boolean
-  return re.match("^%s\.%s\.%s\.%s$" % (unit, unit, unit, unit), ip)
-
-
 def IsValidShellParam(word):
   """Verifies is the given word is safe from the shell's p.o.v.
 
@@ -1287,18 +1301,61 @@ def ParseUnit(input_string):
   return value
 
 
-def AddAuthorizedKey(file_name, key):
+def ParseCpuMask(cpu_mask):
+  """Parse a CPU mask definition and return the list of CPU IDs.
+
+  CPU mask format: comma-separated list of CPU IDs
+  or dash-separated ID ranges
+  Example: "0-2,5" -> "0,1,2,5"
+
+  @type cpu_mask: str
+  @param cpu_mask: CPU mask definition
+  @rtype: list of int
+  @return: list of CPU IDs
+
+  """
+  if not cpu_mask:
+    return []
+  cpu_list = []
+  for range_def in cpu_mask.split(","):
+    boundaries = range_def.split("-")
+    n_elements = len(boundaries)
+    if n_elements > 2:
+      raise errors.ParseError("Invalid CPU ID range definition"
+                              " (only one hyphen allowed): %s" % range_def)
+    try:
+      lower = int(boundaries[0])
+    except (ValueError, TypeError), err:
+      raise errors.ParseError("Invalid CPU ID value for lower boundary of"
+                              " CPU ID range: %s" % str(err))
+    try:
+      higher = int(boundaries[-1])
+    except (ValueError, TypeError), err:
+      raise errors.ParseError("Invalid CPU ID value for higher boundary of"
+                              " CPU ID range: %s" % str(err))
+    if lower > higher:
+      raise errors.ParseError("Invalid CPU ID range definition"
+                              " (%d > %d): %s" % (lower, higher, range_def))
+    cpu_list.extend(range(lower, higher + 1))
+  return cpu_list
+
+
+def AddAuthorizedKey(file_obj, key):
   """Adds an SSH public key to an authorized_keys file.
 
-  @type file_name: str
-  @param file_name: path to authorized_keys file
+  @type file_obj: str or file handle
+  @param file_obj: path to authorized_keys file
   @type key: str
   @param key: string containing key
 
   """
   key_fields = key.split()
 
-  f = open(file_name, 'a+')
+  if isinstance(file_obj, basestring):
+    f = open(file_obj, 'a+')
+  else:
+    f = file_obj
+
   try:
     nl = True
     for line in f:
@@ -1404,7 +1461,7 @@ def AddHostToEtcHosts(hostname):
       L{constants.ETC_HOSTS}
 
   """
-  hi = HostInfo(name=hostname)
+  hi = netutils.HostInfo(name=hostname)
   SetEtcHostsEntry(constants.ETC_HOSTS, hi.ip, hi.name, [hi.ShortName()])
 
 
@@ -1461,7 +1518,7 @@ def RemoveHostFromEtcHosts(hostname):
       L{constants.ETC_HOSTS}
 
   """
-  hi = HostInfo(name=hostname)
+  hi = netutils.HostInfo(name=hostname)
   RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.name)
   RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.ShortName())
 
@@ -1536,66 +1593,46 @@ def ShellQuoteArgs(args):
   return ' '.join([ShellQuote(i) for i in args])
 
 
-def TcpPing(target, port, timeout=10, live_port_needed=False, source=None):
-  """Simple ping implementation using TCP connect(2).
-
-  Check if the given IP is reachable by doing attempting a TCP connect
-  to it.
-
-  @type target: str
-  @param target: the IP or hostname to ping
-  @type port: int
-  @param port: the port to connect to
-  @type timeout: int
-  @param timeout: the timeout on the connection attempt
-  @type live_port_needed: boolean
-  @param live_port_needed: whether a closed port will cause the
-      function to return failure, as if there was a timeout
-  @type source: str or None
-  @param source: if specified, will cause the connect to be made
-      from this specific source address; failures to bind other
-      than C{EADDRNOTAVAIL} will be ignored
+class ShellWriter:
+  """Helper class to write scripts with indentation.
 
   """
-  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+  INDENT_STR = "  "
 
-  success = False
+  def __init__(self, fh):
+    """Initializes this class.
 
-  if source is not None:
-    try:
-      sock.bind((source, 0))
-    except socket.error, (errcode, _):
-      if errcode == errno.EADDRNOTAVAIL:
-        success = False
+    """
+    self._fh = fh
+    self._indent = 0
 
-  sock.settimeout(timeout)
+  def IncIndent(self):
+    """Increase indentation level by 1.
 
-  try:
-    sock.connect((target, port))
-    sock.close()
-    success = True
-  except socket.timeout:
-    success = False
-  except socket.error, (errcode, _):
-    success = (not live_port_needed) and (errcode == errno.ECONNREFUSED)
+    """
+    self._indent += 1
 
-  return success
+  def DecIndent(self):
+    """Decrease indentation level by 1.
 
+    """
+    assert self._indent > 0
+    self._indent -= 1
 
-def OwnIpAddress(address):
-  """Check if the current host has the the given IP address.
+  def Write(self, txt, *args):
+    """Write line to output file.
 
-  Currently this is done by TCP-pinging the address from the loopback
-  address.
+    """
+    assert self._indent >= 0
 
-  @type address: string
-  @param address: the address to check
-  @rtype: bool
-  @return: True if we own the address
+    self._fh.write(self._indent * self.INDENT_STR)
 
-  """
-  return TcpPing(address, constants.DEFAULT_NODED_PORT,
-                 source=constants.LOCALHOST_IP_ADDRESS)
+    if args:
+      self._fh.write(txt % args)
+    else:
+      self._fh.write(txt)
+
+    self._fh.write("\n")
 
 
 def ListVisibleFiles(path):
@@ -1612,7 +1649,6 @@ def ListVisibleFiles(path):
     raise errors.ProgrammerError("Path passed to ListVisibleFiles is not"
                                  " absolute/normalized: '%s'" % path)
   files = [i for i in os.listdir(path) if not i.startswith(".")]
-  files.sort()
   return files
 
 
@@ -1677,6 +1713,11 @@ def EnsureDirs(dirs):
       if err.errno != errno.EEXIST:
         raise errors.GenericError("Cannot create needed directory"
                                   " '%s': %s" % (dir_name, err))
+    try:
+      os.chmod(dir_name, dir_mode)
+    except EnvironmentError, err:
+      raise errors.GenericError("Cannot change directory permissions on"
+                                " '%s': %s" % (dir_name, err))
     if not os.path.isdir(dir_name):
       raise errors.GenericError("%s is not a directory" % dir_name)
 
@@ -1796,6 +1837,24 @@ def WriteFile(file_name, fn=None, data=None,
   return result
 
 
+def ReadOneLineFile(file_name, strict=False):
+  """Return the first non-empty line from a file.
+
+  @type strict: boolean
+  @param strict: if True, abort if the file has more than one
+      non-empty line
+
+  """
+  file_lines = ReadFile(file_name).splitlines()
+  full_lines = filter(bool, file_lines)
+  if not file_lines or not full_lines:
+    raise errors.GenericError("No data in one-liner file %s" % file_name)
+  elif strict and len(full_lines) > 1:
+    raise errors.GenericError("Too many lines in one-liner file %s" %
+                              file_name)
+  return full_lines[0]
+
+
 def FirstFree(seq, base=0):
   """Returns the first non-existing integer from seq.
 
@@ -2019,7 +2078,41 @@ def CloseFDs(noclose_fds=None):
     _CloseFDNoErr(fd)
 
 
-def Daemonize(logfile):
+def Mlockall(_ctypes=ctypes):
+  """Lock current process' virtual address space into RAM.
+
+  This is equivalent to the C call mlockall(MCL_CURRENT|MCL_FUTURE),
+  see mlock(2) for more details. This function requires ctypes module.
+
+  @raises errors.NoCtypesError: if ctypes module is not found
+
+  """
+  if _ctypes is None:
+    raise errors.NoCtypesError()
+
+  libc = _ctypes.cdll.LoadLibrary("libc.so.6")
+  if libc is None:
+    logging.error("Cannot set memory lock, ctypes cannot load libc")
+    return
+
+  # Some older version of the ctypes module don't have built-in functionality
+  # to access the errno global variable, where function error codes are stored.
+  # By declaring this variable as a pointer to an integer we can then access
+  # its value correctly, should the mlockall call fail, in order to see what
+  # the actual error code was.
+  # pylint: disable-msg=W0212
+  libc.__errno_location.restype = _ctypes.POINTER(_ctypes.c_int)
+
+  if libc.mlockall(_MCL_CURRENT | _MCL_FUTURE):
+    # pylint: disable-msg=W0212
+    logging.error("Cannot set memory lock: %s",
+                  os.strerror(libc.__errno_location().contents.value))
+    return
+
+  logging.debug("Memory lock set")
+
+
+def Daemonize(logfile, run_uid, run_gid):
   """Daemonize the current process.
 
   This detaches the current process from the controlling terminal and
@@ -2027,6 +2120,10 @@ def Daemonize(logfile):
 
   @type logfile: str
   @param logfile: the logfile to which we should redirect stdout/stderr
+  @type run_uid: int
+  @param run_uid: Run the child under this uid
+  @type run_gid: int
+  @param run_gid: Run the child under this gid
   @rtype: int
   @return: the value zero
 
@@ -2040,6 +2137,11 @@ def Daemonize(logfile):
   pid = os.fork()
   if (pid == 0):  # The first child.
     os.setsid()
+    # FIXME: When removing again and moving to start-stop-daemon privilege drop
+    #        make sure to check for config permission and bail out when invoked
+    #        with wrong user.
+    os.setgid(run_gid)
+    os.setuid(run_uid)
     # this might fail
     pid = os.fork() # Fork a second child.
     if (pid == 0):  # The second child.
@@ -2088,6 +2190,19 @@ def EnsureDaemon(name):
   return True
 
 
+def StopDaemon(name):
+  """Stop daemon
+
+  """
+  result = RunCmd([constants.DAEMON_UTIL, "stop", name])
+  if result.failed:
+    logging.error("Can't stop daemon '%s', failure %s, output: %s",
+                  name, result.fail_reason, result.output)
+    return False
+
+  return True
+
+
 def WritePidFile(name):
   """Write the current process pidfile.
 
@@ -2144,8 +2259,7 @@ def KillProcess(pid, signal_=signal.SIGTERM, timeout=30,
   """
   def _helper(pid, signal_, wait):
     """Simple helper to encapsulate the kill/waitpid sequence"""
-    os.kill(pid, signal_)
-    if wait:
+    if IgnoreProcessNotFound(os.kill, pid, signal_) and wait:
       try:
         os.waitpid(pid, os.WNOHANG)
       except OSError:
@@ -2283,32 +2397,43 @@ def MergeTime(timetuple):
   return float(seconds) + (float(microseconds) * 0.000001)
 
 
-def GetDaemonPort(daemon_name):
-  """Get the daemon port for this cluster.
-
-  Note that this routine does not read a ganeti-specific file, but
-  instead uses C{socket.getservbyname} to allow pre-customization of
-  this parameter outside of Ganeti.
+class LogFileHandler(logging.FileHandler):
+  """Log handler that doesn't fallback to stderr.
 
-  @type daemon_name: string
-  @param daemon_name: daemon name (in constants.DAEMONS_PORTS)
-  @rtype: int
+  When an error occurs while writing on the logfile, logging.FileHandler tries
+  to log on stderr. This doesn't work in ganeti since stderr is redirected to
+  the logfile. This class avoids failures reporting errors to /dev/console.
 
   """
-  if daemon_name not in constants.DAEMONS_PORTS:
-    raise errors.ProgrammerError("Unknown daemon: %s" % daemon_name)
+  def __init__(self, filename, mode="a", encoding=None):
+    """Open the specified file and use it as the stream for logging.
 
-  (proto, default_port) = constants.DAEMONS_PORTS[daemon_name]
-  try:
-    port = socket.getservbyname(daemon_name, proto)
-  except socket.error:
-    port = default_port
+    Also open /dev/console to report errors while logging.
 
-  return port
+    """
+    logging.FileHandler.__init__(self, filename, mode, encoding)
+    self.console = open(constants.DEV_CONSOLE, "a")
+
+  def handleError(self, record): # pylint: disable-msg=C0103
+    """Handle errors which occur during an emit() call.
+
+    Try to handle errors with FileHandler method, if it fails write to
+    /dev/console.
+
+    """
+    try:
+      logging.FileHandler.handleError(self, record)
+    except Exception: # pylint: disable-msg=W0703
+      try:
+        self.console.write("Cannot log message:\n%s\n" % self.format(record))
+      except Exception: # pylint: disable-msg=W0703
+        # Log handler tried everything it could, now just give up
+        pass
 
 
 def SetupLogging(logfile, debug=0, stderr_logging=False, program="",
-                 multithreaded=False, syslog=constants.SYSLOG_USAGE):
+                 multithreaded=False, syslog=constants.SYSLOG_USAGE,
+                 console_logging=False):
   """Configures the logging module.
 
   @type logfile: str
@@ -2327,6 +2452,9 @@ def SetupLogging(logfile, debug=0, stderr_logging=False, program="",
       - if no, syslog is not used
       - if yes, syslog is used (in addition to file-logging)
       - if only, only syslog is used
+  @type console_logging: boolean
+  @param console_logging: if True, will use a FileHandler which falls back to
+      the system console if logging fails
   @raise EnvironmentError: if we can't open the log file and
       syslog/stderr logging is disabled
 
@@ -2378,7 +2506,10 @@ def SetupLogging(logfile, debug=0, stderr_logging=False, program="",
     # the error if stderr_logging is True, and if false we re-raise the
     # exception since otherwise we could run but without any logs at all
     try:
-      logfile_handler = logging.FileHandler(logfile)
+      if console_logging:
+        logfile_handler = LogFileHandler(logfile)
+      else:
+        logfile_handler = logging.FileHandler(logfile)
       logfile_handler.setFormatter(formatter)
       if debug:
         logfile_handler.setLevel(logging.DEBUG)
@@ -2680,7 +2811,7 @@ def Sha1Hmac(key, text, salt=None):
   else:
     salted_text = text
 
-  return hmac.new(key, salted_text, sha1).hexdigest()
+  return hmac.new(key, salted_text, compat.sha1).hexdigest()
 
 
 def VerifySha1Hmac(key, text, digest, salt=None):
@@ -2820,6 +2951,26 @@ def CalculateDirectorySize(path):
   return BytesToMebibyte(size)
 
 
+def GetMounts(filename=constants.PROC_MOUNTS):
+  """Returns the list of mounted filesystems.
+
+  This function is Linux-specific.
+
+  @param filename: path of mounts file (/proc/mounts by default)
+  @rtype: list of tuples
+  @return: list of mount entries (device, mountpoint, fstype, options)
+
+  """
+  # TODO(iustin): investigate non-Linux options (e.g. via mount output)
+  data = []
+  mountlines = ReadFile(filename).splitlines()
+  for line in mountlines:
+    device, mountpoint, fstype, options, _ = line.split(None, 4)
+    data.append((device, mountpoint, fstype, options))
+
+  return data
+
+
 def GetFilesystemStats(path):
   """Returns the total and free space on a filesystem.
 
@@ -2883,32 +3034,44 @@ def RunInSeparateProcess(fn, *args):
   return bool(exitcode)
 
 
-def LockedMethod(fn):
-  """Synchronized object access decorator.
+def IgnoreProcessNotFound(fn, *args, **kwargs):
+  """Ignores ESRCH when calling a process-related function.
 
-  This decorator is intended to protect access to an object using the
-  object's own lock which is hardcoded to '_lock'.
+  ESRCH is raised when a process is not found.
+
+  @rtype: bool
+  @return: Whether process was found
 
   """
-  def _LockDebug(*args, **kwargs):
-    if debug_locks:
-      logging.debug(*args, **kwargs)
+  try:
+    fn(*args, **kwargs)
+  except EnvironmentError, err:
+    # Ignore ESRCH
+    if err.errno == errno.ESRCH:
+      return False
+    raise
+
+  return True
 
-  def wrapper(self, *args, **kwargs):
-    # pylint: disable-msg=W0212
-    assert hasattr(self, '_lock')
-    lock = self._lock
-    _LockDebug("Waiting for %s", lock)
-    lock.acquire()
-    try:
-      _LockDebug("Acquired %s", lock)
-      result = fn(self, *args, **kwargs)
-    finally:
-      _LockDebug("Releasing %s", lock)
-      lock.release()
-      _LockDebug("Released %s", lock)
-    return result
-  return wrapper
+
+def IgnoreSignals(fn, *args, **kwargs):
+  """Tries to call a function ignoring failures due to EINTR.
+
+  """
+  try:
+    return fn(*args, **kwargs)
+  except EnvironmentError, err:
+    if err.errno == errno.EINTR:
+      return None
+    else:
+      raise
+  except (select.error, socket.error), err:
+    # In python 2.6 and above select.error is an IOError, so it's handled
+    # above, in 2.5 and below it's not, and it's handled here.
+    if err.args and err.args[0] == errno.EINTR:
+      return None
+    else:
+      raise
 
 
 def LockFile(fd):
@@ -2941,6 +3104,31 @@ def FormatTime(val):
   return time.strftime("%F %T", time.localtime(val))
 
 
+def FormatSeconds(secs):
+  """Formats seconds for easier reading.
+
+  @type secs: number
+  @param secs: Number of seconds
+  @rtype: string
+  @return: Formatted seconds (e.g. "2d 9h 19m 49s")
+
+  """
+  parts = []
+
+  secs = round(secs, 0)
+
+  if secs > 0:
+    # Negative values would be a bit tricky
+    for unit, one in [("d", 24 * 60 * 60), ("h", 60 * 60), ("m", 60)]:
+      (complete, secs) = divmod(secs, one)
+      if complete or parts:
+        parts.append("%d%s" % (complete, unit))
+
+  parts.append("%ds" % secs)
+
+  return " ".join(parts)
+
+
 def ReadWatcherPauseFile(filename, now=None, remove_after=3600):
   """Reads the watcher pause file.
 
@@ -3179,11 +3367,22 @@ def GenerateSelfSignedX509Cert(common_name, validity):
   return (key_pem, cert_pem)
 
 
-def GenerateSelfSignedSslCert(filename, validity=(5 * 365)):
+def GenerateSelfSignedSslCert(filename, common_name=constants.X509_CERT_CN,
+                              validity=constants.X509_CERT_DEFAULT_VALIDITY):
   """Legacy function to generate self-signed X509 certificate.
 
+  @type filename: str
+  @param filename: path to write certificate to
+  @type common_name: string
+  @param common_name: commonName value
+  @type validity: int
+  @param validity: validity of certificate in number of days
+
   """
-  (key_pem, cert_pem) = GenerateSelfSignedX509Cert(None,
+  # TODO: Investigate using the cluster name instead of X505_CERT_CN for
+  # common_name, as cluster-renames are very seldom, and it'd be nice if RAPI
+  # and node daemon certificates have the proper Subject/Issuer.
+  (key_pem, cert_pem) = GenerateSelfSignedX509Cert(common_name,
                                                    validity * 24 * 60 * 60)
 
   WriteFile(filename, mode=0400, data=key_pem + cert_pem)