X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/8e5a705d239f0d3740a6d539ae8e9d9d12115615..c4929a8bcca4a43dc6434394a91a8ea67d854844:/lib/utils/io.py diff --git a/lib/utils/io.py b/lib/utils/io.py index 8ed204c..7e6fab8 100644 --- a/lib/utils/io.py +++ b/lib/utils/io.py @@ -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())