gnt-cluster: Add hv/disk state to init
[ganeti-local] / lib / utils / io.py
index 8ed204c..7e6fab8 100644 (file)
@@ -28,6 +28,7 @@ import shutil
 import tempfile
 import errno
 import time
+import stat
 
 from ganeti import errors
 from ganeti import constants
@@ -38,17 +39,40 @@ from ganeti.utils import filelock
 _RANDOM_UUID_FILE = "/proc/sys/kernel/random/uuid"
 
 
-def ReadFile(file_name, size=-1):
+def ErrnoOrStr(err):
+  """Format an EnvironmentError exception.
+
+  If the L{err} argument has an errno attribute, it will be looked up
+  and converted into a textual C{E...} description. Otherwise the
+  string representation of the error will be returned.
+
+  @type err: L{EnvironmentError}
+  @param err: the exception to format
+
+  """
+  if hasattr(err, "errno"):
+    detail = errno.errorcode[err.errno]
+  else:
+    detail = str(err)
+  return detail
+
+
+def ReadFile(file_name, size=-1, preread=None):
   """Reads a file.
 
   @type size: int
   @param size: Read at most size bytes (if negative, entire file)
+  @type preread: callable receiving file handle as single parameter
+  @param preread: Function called before file is read
   @rtype: str
   @return: the (possibly partial) content of the file
 
   """
   f = open(file_name, "r")
   try:
+    if preread:
+      preread(f)
+
     return f.read(size)
   finally:
     f.close()
@@ -294,6 +318,9 @@ def RenameFile(old, new, mkdir=False, mkdir_mode=0750, dir_uid=None,
                dir_gid=None):
   """Renames a file.
 
+  This just creates the very least directory if it does not exist and C{mkdir}
+  is set to true.
+
   @type old: string
   @param old: Original path
   @type new: string
@@ -317,15 +344,88 @@ def RenameFile(old, new, mkdir=False, mkdir_mode=0750, dir_uid=None,
     if mkdir and err.errno == errno.ENOENT:
       # Create directory and try again
       dir_path = os.path.dirname(new)
-      Makedirs(dir_path, mode=mkdir_mode)
-      if not (dir_uid is None or dir_gid is None):
-        os.chown(dir_path, dir_uid, dir_gid)
+      MakeDirWithPerm(dir_path, mkdir_mode, dir_uid, dir_gid)
 
       return os.rename(old, new)
 
     raise
 
 
+def EnforcePermission(path, mode, uid=None, gid=None, must_exist=True,
+                      _chmod_fn=os.chmod, _chown_fn=os.chown, _stat_fn=os.stat):
+  """Enforces that given path has given permissions.
+
+  @param path: The path to the file
+  @param mode: The mode of the file
+  @param uid: The uid of the owner of this file
+  @param gid: The gid of the owner of this file
+  @param must_exist: Specifies if non-existance of path will be an error
+  @param _chmod_fn: chmod function to use (unittest only)
+  @param _chown_fn: chown function to use (unittest only)
+
+  """
+  logging.debug("Checking %s", path)
+
+  # chown takes -1 if you want to keep one part of the ownership, however
+  # None is Python standard for that. So we remap them here.
+  if uid is None:
+    uid = -1
+  if gid is None:
+    gid = -1
+
+  try:
+    st = _stat_fn(path)
+
+    fmode = stat.S_IMODE(st[stat.ST_MODE])
+    if fmode != mode:
+      logging.debug("Changing mode of %s from %#o to %#o", path, fmode, mode)
+      _chmod_fn(path, mode)
+
+    if max(uid, gid) > -1:
+      fuid = st[stat.ST_UID]
+      fgid = st[stat.ST_GID]
+      if fuid != uid or fgid != gid:
+        logging.debug("Changing owner of %s from UID %s/GID %s to"
+                      " UID %s/GID %s", path, fuid, fgid, uid, gid)
+        _chown_fn(path, uid, gid)
+  except EnvironmentError, err:
+    if err.errno == errno.ENOENT:
+      if must_exist:
+        raise errors.GenericError("Path %s should exist, but does not" % path)
+    else:
+      raise errors.GenericError("Error while changing permissions on %s: %s" %
+                                (path, err))
+
+
+def MakeDirWithPerm(path, mode, uid, gid, _lstat_fn=os.lstat,
+                    _mkdir_fn=os.mkdir, _perm_fn=EnforcePermission):
+  """Enforces that given path is a dir and has given mode, uid and gid set.
+
+  @param path: The path to the file
+  @param mode: The mode of the file
+  @param uid: The uid of the owner of this file
+  @param gid: The gid of the owner of this file
+  @param _lstat_fn: Stat function to use (unittest only)
+  @param _mkdir_fn: mkdir function to use (unittest only)
+  @param _perm_fn: permission setter function to use (unittest only)
+
+  """
+  logging.debug("Checking directory %s", path)
+  try:
+    # We don't want to follow symlinks
+    st = _lstat_fn(path)
+  except EnvironmentError, err:
+    if err.errno != errno.ENOENT:
+      raise errors.GenericError("stat(2) on %s failed: %s" % (path, err))
+    _mkdir_fn(path)
+  else:
+    if not stat.S_ISDIR(st[stat.ST_MODE]):
+      raise errors.GenericError(("Path %s is expected to be a directory, but "
+                                 "isn't") % path)
+
+  _perm_fn(path, mode, uid=uid, gid=gid)
+
+
 def Makedirs(path, mode=0750):
   """Super-mkdir; create a leaf directory and all intermediate ones.
 
@@ -370,10 +470,10 @@ def CreateBackup(file_name):
             (os.path.basename(file_name), TimestampForFilename()))
   dir_name = os.path.dirname(file_name)
 
-  fsrc = open(file_name, 'rb')
+  fsrc = open(file_name, "rb")
   try:
     (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
-    fdst = os.fdopen(fd, 'wb')
+    fdst = os.fdopen(fd, "wb")
     try:
       logging.debug("Backing up %s at %s", file_name, backup_name)
       shutil.copyfileobj(fsrc, fdst)
@@ -468,6 +568,20 @@ def IsNormAbsPath(path):
   return os.path.normpath(path) == path and os.path.isabs(path)
 
 
+def IsBelowDir(root, other_path):
+  """Check whether a path is below a root dir.
+
+  This works around the nasty byte-byte comparisation of commonprefix.
+
+  """
+  if not (os.path.isabs(root) and os.path.isabs(other_path)):
+    raise ValueError("Provided paths '%s' and '%s' are not absolute" %
+                     (root, other_path))
+  prepared_root = "%s%s" % (os.path.normpath(root), os.sep)
+  return os.path.commonprefix([prepared_root,
+                               os.path.normpath(other_path)]) == prepared_root
+
+
 def PathJoin(*args):
   """Safe-join a list of path components.
 
@@ -491,10 +605,9 @@ def PathJoin(*args):
   if not IsNormAbsPath(result):
     raise ValueError("Invalid parameters to PathJoin: '%s'" % str(args))
   # check that we're still under the original prefix
-  prefix = os.path.commonprefix([root, result])
-  if prefix != root:
+  if not IsBelowDir(root, result):
     raise ValueError("Error: path joining resulted in different prefix"
-                     " (%s != %s)" % (prefix, root))
+                     " (%s != %s)" % (result, root))
   return result
 
 
@@ -514,7 +627,7 @@ def TailFile(fname, lines=20):
   try:
     fd.seek(0, 2)
     pos = fd.tell()
-    pos = max(0, pos-4096)
+    pos = max(0, pos - 4096)
     fd.seek(pos, 0)
     raw_data = fd.read()
   finally:
@@ -640,7 +753,7 @@ def AddAuthorizedKey(file_obj, key):
   key_fields = key.split()
 
   if isinstance(file_obj, basestring):
-    f = open(file_obj, 'a+')
+    f = open(file_obj, "a+")
   else:
     f = file_obj
 
@@ -650,11 +763,11 @@ def AddAuthorizedKey(file_obj, key):
       # Ignore whitespace changes
       if line.split() == key_fields:
         break
-      nl = line.endswith('\n')
+      nl = line.endswith("\n")
     else:
       if not nl:
         f.write("\n")
-      f.write(key.rstrip('\r\n'))
+      f.write(key.rstrip("\r\n"))
       f.write("\n")
       f.flush()
   finally:
@@ -674,9 +787,9 @@ def RemoveAuthorizedKey(file_name, key):
 
   fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
   try:
-    out = os.fdopen(fd, 'w')
+    out = os.fdopen(fd, "w")
     try:
-      f = open(file_name, 'r')
+      f = open(file_name, "r")
       try:
         for line in f:
           # Ignore whitespace changes while comparing lines
@@ -786,3 +899,40 @@ def NewUUID():
 
   """
   return ReadFile(_RANDOM_UUID_FILE, size=128).rstrip("\n")
+
+
+class TemporaryFileManager(object):
+  """Stores the list of files to be deleted and removes them on demand.
+
+  """
+
+  def __init__(self):
+    self._files = []
+
+  def __del__(self):
+    self.Cleanup()
+
+  def Add(self, filename):
+    """Add file to list of files to be deleted.
+
+    @type filename: string
+    @param filename: path to filename to be added
+
+    """
+    self._files.append(filename)
+
+  def Remove(self, filename):
+    """Remove file from list of files to be deleted.
+
+    @type filename: string
+    @param filename: path to filename to be deleted
+
+    """
+    self._files.remove(filename)
+
+  def Cleanup(self):
+    """Delete all files marked for deletion
+
+    """
+    while self._files:
+      RemoveFile(self._files.pop())