Initial copy of RAPI filebase to the trunk
[ganeti-local] / lib / utils.py
index 7aa75e1..ceacc9b 100644 (file)
@@ -39,10 +39,10 @@ import itertools
 import select
 import fcntl
 import resource
+import logging
 
 from cStringIO import StringIO
 
-from ganeti import logger
 from ganeti import errors
 from ganeti import constants
 
@@ -88,9 +88,9 @@ class RunResult(object):
     else:
       self.fail_reason = "unable to determine termination reason"
 
-    if debug and self.failed:
-      logger.Debug("Command '%s' failed (%s); output: %s" %
-                   (self.cmd, self.fail_reason, self.output))
+    if self.failed:
+      logging.debug("Command '%s' failed (%s); output: %s",
+                    self.cmd, self.fail_reason, self.output)
 
   def _GetOutput(self):
     """Returns the combined stdout and stderr for easier usage.
@@ -101,113 +101,6 @@ class RunResult(object):
   output = property(_GetOutput, None, None, "Return full output")
 
 
-def _GetLockFile(subsystem):
-  """Compute the file name for a given lock name."""
-  return "%s/ganeti_lock_%s" % (constants.LOCK_DIR, subsystem)
-
-
-def Lock(name, max_retries=None, debug=False):
-  """Lock a given subsystem.
-
-  In case the lock is already held by an alive process, the function
-  will sleep indefintely and poll with a one second interval.
-
-  When the optional integer argument 'max_retries' is passed with a
-  non-zero value, the function will sleep only for this number of
-  times, and then it will will raise a LockError if the lock can't be
-  acquired. Passing in a negative number will cause only one try to
-  get the lock. Passing a positive number will make the function retry
-  for approximately that number of seconds.
-
-  """
-  lockfile = _GetLockFile(name)
-
-  if name in _locksheld:
-    raise errors.LockError('Lock "%s" already held!' % (name,))
-
-  errcount = 0
-
-  retries = 0
-  while True:
-    try:
-      fd = os.open(lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR | os.O_SYNC)
-      break
-    except OSError, creat_err:
-      if creat_err.errno != errno.EEXIST:
-        raise errors.LockError("Can't create the lock file. Error '%s'." %
-                               str(creat_err))
-
-      try:
-        pf = open(lockfile, 'r')
-      except IOError, open_err:
-        errcount += 1
-        if errcount >= 5:
-          raise errors.LockError("Lock file exists but cannot be opened."
-                                 " Error: '%s'." % str(open_err))
-        time.sleep(1)
-        continue
-
-      try:
-        pid = int(pf.read())
-      except ValueError:
-        raise errors.LockError("Invalid pid string in %s" %
-                               (lockfile,))
-
-      if not IsProcessAlive(pid):
-        raise errors.LockError("Stale lockfile %s for pid %d?" %
-                               (lockfile, pid))
-
-      if max_retries and max_retries <= retries:
-        raise errors.LockError("Can't acquire lock during the specified"
-                               " time, aborting.")
-      if retries == 5 and (debug or sys.stdin.isatty()):
-        logger.ToStderr("Waiting for '%s' lock from pid %d..." % (name, pid))
-
-      time.sleep(1)
-      retries += 1
-      continue
-
-  os.write(fd, '%d\n' % (os.getpid(),))
-  os.close(fd)
-
-  _locksheld.append(name)
-
-
-def Unlock(name):
-  """Unlock a given subsystem.
-
-  """
-  lockfile = _GetLockFile(name)
-
-  try:
-    fd = os.open(lockfile, os.O_RDONLY)
-  except OSError:
-    raise errors.LockError('Lock "%s" not held.' % (name,))
-
-  f = os.fdopen(fd, 'r')
-  pid_str = f.read()
-
-  try:
-    pid = int(pid_str)
-  except ValueError:
-    raise errors.LockError('Unable to determine PID of locking process.')
-
-  if pid != os.getpid():
-    raise errors.LockError('Lock not held by me (%d != %d)' %
-                           (os.getpid(), pid,))
-
-  os.unlink(lockfile)
-  _locksheld.remove(name)
-
-
-def LockCleanup():
-  """Remove all locks.
-
-  """
-  for lock in _locksheld:
-    Unlock(lock)
-
-
 def RunCmd(cmd):
   """Execute a (shell) command.
 
@@ -230,6 +123,7 @@ def RunCmd(cmd):
   else:
     strcmd = cmd
     shell = True
+  logging.debug("RunCmd '%s'", strcmd)
   env = os.environ.copy()
   env["LC_ALL"] = "C"
   poller = select.poll()
@@ -281,30 +175,6 @@ def RunCmd(cmd):
   return RunResult(exitcode, signal, out, err, strcmd)
 
 
-def RunCmdUnlocked(cmd):
-  """Execute a shell command without the 'cmd' lock.
-
-  This variant of `RunCmd()` drops the 'cmd' lock before running the
-  command and re-aquires it afterwards, thus it can be used to call
-  other ganeti commands.
-
-  The argument and return values are the same as for the `RunCmd()`
-  function.
-
-  Args:
-    cmd - command to run. (str)
-
-  Returns:
-    `RunResult`
-
-  """
-  Unlock('cmd')
-  ret = RunCmd(cmd)
-  Lock('cmd')
-
-  return ret
-
-
 def RemoveFile(filename):
   """Remove a file ignoring some errors.
 
@@ -389,8 +259,7 @@ def CheckDict(target, template, logname=None):
       target[k] = template[k]
 
   if missing and logname:
-    logger.Debug('%s missing keys %s' %
-                 (logname, ', '.join(missing)))
+    logging.warning('%s missing keys %s', logname, ', '.join(missing))
 
 
 def IsProcessAlive(pid):
@@ -517,7 +386,7 @@ def ListVolumeGroups():
       name, size = line.split()
       size = int(float(size))
     except (IndexError, ValueError), err:
-      logger.Error("Invalid output from vgs (%s): %s" % (err, line))
+      logging.error("Invalid output from vgs (%s): %s", err, line)
       continue
 
     retval[name] = size
@@ -781,6 +650,14 @@ def SetEtcHostsEntry(file_name, ip, hostname, aliases):
     raise
 
 
+def AddHostToEtcHosts(hostname):
+  """Wrapper around SetEtcHostsEntry.
+
+  """
+  hi = HostInfo(name=hostname)
+  SetEtcHostsEntry(constants.ETC_HOSTS, hi.ip, hi.name, [hi.ShortName()])
+
+
 def RemoveEtcHostsEntry(file_name, hostname):
   """Removes a hostname from /etc/hosts.
 
@@ -817,6 +694,15 @@ def RemoveEtcHostsEntry(file_name, hostname):
     raise
 
 
+def RemoveHostFromEtcHosts(hostname):
+  """Wrapper around RemoveEtcHostsEntry.
+
+  """
+  hi = HostInfo(name=hostname)
+  RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.name)
+  RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.ShortName())
+
+
 def CreateBackup(file_name):
   """Creates a backup of a file.
 
@@ -942,8 +828,9 @@ def NewUUID():
 
 def WriteFile(file_name, fn=None, data=None,
               mode=None, uid=-1, gid=-1,
-              atime=None, mtime=None,
-              check_abspath=True, dry_run=False, backup=False):
+              atime=None, mtime=None, close=True,
+              dry_run=False, backup=False,
+              prewrite=None, postwrite=None):
   """(Over)write a file atomically.
 
   The file_name and either fn (a function taking one argument, the
@@ -957,8 +844,24 @@ def WriteFile(file_name, fn=None, data=None,
   exception, an existing target file should be unmodified and the
   temporary file should be removed.
 
+  Args:
+    file_name: New filename
+    fn: Content writing function, called with file descriptor as parameter
+    data: Content as string
+    mode: File mode
+    uid: Owner
+    gid: Group
+    atime: Access time
+    mtime: Modification time
+    close: Whether to close file after writing it
+    prewrite: Function object called before writing content
+    postwrite: Function object called after writing content
+
+  Returns:
+    None if "close" parameter evaluates to True, otherwise file descriptor.
+
   """
-  if check_abspath and not os.path.isabs(file_name):
+  if not os.path.isabs(file_name):
     raise errors.ProgrammerError("Path passed to WriteFile is not"
                                  " absolute: '%s'" % file_name)
 
@@ -981,19 +884,50 @@ def WriteFile(file_name, fn=None, data=None,
       os.chown(new_name, uid, gid)
     if mode:
       os.chmod(new_name, mode)
+    if callable(prewrite):
+      prewrite(fd)
     if data is not None:
       os.write(fd, data)
     else:
       fn(fd)
+    if callable(postwrite):
+      postwrite(fd)
     os.fsync(fd)
     if atime is not None and mtime is not None:
       os.utime(new_name, (atime, mtime))
     if not dry_run:
       os.rename(new_name, file_name)
   finally:
-    os.close(fd)
+    if close:
+      os.close(fd)
+      result = None
+    else:
+      result = fd
     RemoveFile(new_name)
 
+  return result
+
+
+def FirstFree(seq, base=0):
+  """Returns the first non-existing integer from seq.
+
+  The seq argument should be a sorted list of positive integers. The
+  first time the index of an element is smaller than the element
+  value, the index will be returned.
+
+  The base argument is used to start at a different offset,
+  i.e. [3, 4, 6] with offset=3 will return 5.
+
+  Example: [0, 1, 3] will return 2.
+
+  """
+  for idx, elem in enumerate(seq):
+    assert elem >= base, "Passed element is higher than base offset"
+    if elem > idx + base:
+      # idx is not used
+      return idx + base
+  return None
+
 
 def all(seq, pred=bool):
   "Returns True if pred(x) is True for every element in the iterable"
@@ -1113,3 +1047,50 @@ def FindFile(name, search_path, test=os.path.exists):
     if test(item_name):
       return item_name
   return None
+
+
+def CheckVolumeGroupSize(vglist, vgname, minsize):
+  """Checks if the volume group list is valid.
+
+  A non-None return value means there's an error, and the return value
+  is the error message.
+
+  """
+  vgsize = vglist.get(vgname, None)
+  if vgsize is None:
+    return "volume group '%s' missing" % vgname
+  elif vgsize < minsize:
+    return ("volume group '%s' too small (%s MiB required, %d MiB found)" %
+            (vgname, minsize, vgsize))
+  return None
+
+
+def LockedMethod(fn):
+  """Synchronized object access decorator.
+
+  This decorator is intended to protect access to an object using the
+  object's own lock which is hardcoded to '_lock'.
+
+  """
+  def wrapper(self, *args, **kwargs):
+    assert hasattr(self, '_lock')
+    lock = self._lock
+    lock.acquire()
+    try:
+      result = fn(self, *args, **kwargs)
+    finally:
+      lock.release()
+    return result
+  return wrapper
+
+
+def LockFile(fd):
+  """Locks a file using POSIX locks.
+
+  """
+  try:
+    fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
+  except IOError, err:
+    if err.errno == errno.EAGAIN:
+      raise errors.LockError("File already locked")
+    raise