| # |
| # |
| |
| # Copyright (C) 2006, 2007, 2010, 2011, 2012 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 |
| # the Free Software Foundation; either version 2 of the License, or |
| # (at your option) any later version. |
| # |
| # This program is distributed in the hope that it will be useful, but |
| # WITHOUT ANY WARRANTY; without even the implied warranty of |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| # General Public License for more details. |
| # |
| # You should have received a copy of the GNU General Public License |
| # along with this program; if not, write to the Free Software |
| # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
| # 02110-1301, USA. |
| |
| """Utility functions for I/O. |
| |
| """ |
| |
| import os |
| import logging |
| import shutil |
| import tempfile |
| import errno |
| import time |
| import stat |
| import grp |
| import pwd |
| |
| from ganeti import errors |
| from ganeti import constants |
| from ganeti import pathutils |
| from ganeti.utils import filelock |
| |
| #: Directory used by fsck(8) to store recovered data, usually at a file |
| #: system's root directory |
| _LOST_AND_FOUND = "lost+found" |
| |
| # Possible values for keep_perms in WriteFile() |
| KP_NEVER = 0 |
| KP_ALWAYS = 1 |
| KP_IF_EXISTS = 2 |
| |
| KEEP_PERMS_VALUES = [ |
| KP_NEVER, |
| KP_ALWAYS, |
| KP_IF_EXISTS, |
| ] |
| |
| |
| 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 |
| |
| |
| class FileStatHelper: |
| """Helper to store file handle's C{fstat}. |
| |
| Useful in combination with L{ReadFile}'s C{preread} parameter. |
| |
| """ |
| def __init__(self): |
| """Initializes this class. |
| |
| """ |
| self.st = None |
| |
| def __call__(self, fh): |
| """Calls C{fstat} on file handle. |
| |
| """ |
| self.st = os.fstat(fh.fileno()) |
| |
| |
| 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() |
| |
| |
| def WriteFile(file_name, fn=None, data=None, |
| mode=None, uid=-1, gid=-1, |
| atime=None, mtime=None, close=True, |
| dry_run=False, backup=False, |
| prewrite=None, postwrite=None, keep_perms=KP_NEVER): |
| """(Over)write a file atomically. |
| |
| The file_name and either fn (a function taking one argument, the |
| file descriptor, and which should write the data to it) or data (the |
| contents of the file) must be passed. The other arguments are |
| optional and allow setting the file mode, owner and group, and the |
| mtime/atime of the file. |
| |
| If the function doesn't raise an exception, it has succeeded and the |
| target file has the new contents. If the function has raised an |
| exception, an existing target file should be unmodified and the |
| temporary file should be removed. |
| |
| @type file_name: str |
| @param file_name: the target filename |
| @type fn: callable |
| @param fn: content writing function, called with |
| file descriptor as parameter |
| @type data: str |
| @param data: contents of the file |
| @type mode: int |
| @param mode: file mode |
| @type uid: int |
| @param uid: the owner of the file |
| @type gid: int |
| @param gid: the group of the file |
| @type atime: int |
| @param atime: a custom access time to be set on the file |
| @type mtime: int |
| @param mtime: a custom modification time to be set on the file |
| @type close: boolean |
| @param close: whether to close file after writing it |
| @type prewrite: callable |
| @param prewrite: function to be called before writing content |
| @type postwrite: callable |
| @param postwrite: function to be called after writing content |
| @type keep_perms: members of L{KEEP_PERMS_VALUES} |
| @param keep_perms: if L{KP_NEVER} (default), owner, group, and mode are |
| taken from the other parameters; if L{KP_ALWAYS}, owner, group, and |
| mode are copied from the existing file; if L{KP_IF_EXISTS}, owner, |
| group, and mode are taken from the file, and if the file doesn't |
| exist, they are taken from the other parameters. It is an error to |
| pass L{KP_ALWAYS} when the file doesn't exist or when C{uid}, C{gid}, |
| or C{mode} are set to non-default values. |
| |
| @rtype: None or int |
| @return: None if the 'close' parameter evaluates to True, |
| otherwise the file descriptor |
| |
| @raise errors.ProgrammerError: if any of the arguments are not valid |
| |
| """ |
| if not os.path.isabs(file_name): |
| raise errors.ProgrammerError("Path passed to WriteFile is not" |
| " absolute: '%s'" % file_name) |
| |
| if [fn, data].count(None) != 1: |
| raise errors.ProgrammerError("fn or data required") |
| |
| if [atime, mtime].count(None) == 1: |
| raise errors.ProgrammerError("Both atime and mtime must be either" |
| " set or None") |
| |
| if not keep_perms in KEEP_PERMS_VALUES: |
| raise errors.ProgrammerError("Invalid value for keep_perms: %s" % |
| keep_perms) |
| if keep_perms == KP_ALWAYS and (uid != -1 or gid != -1 or mode is not None): |
| raise errors.ProgrammerError("When keep_perms==KP_ALWAYS, 'uid', 'gid'," |
| " and 'mode' cannot be set") |
| |
| if backup and not dry_run and os.path.isfile(file_name): |
| CreateBackup(file_name) |
| |
| if keep_perms == KP_ALWAYS or keep_perms == KP_IF_EXISTS: |
| # os.stat() raises an exception if the file doesn't exist |
| try: |
| file_stat = os.stat(file_name) |
| mode = stat.S_IMODE(file_stat.st_mode) |
| uid = file_stat.st_uid |
| gid = file_stat.st_gid |
| except OSError: |
| if keep_perms == KP_ALWAYS: |
| raise |
| # else: if keeep_perms == KP_IF_EXISTS it's ok if the file doesn't exist |
| |
| # Whether temporary file needs to be removed (e.g. if any error occurs) |
| do_remove = True |
| |
| # Function result |
| result = None |
| |
| (dir_name, base_name) = os.path.split(file_name) |
| (fd, new_name) = tempfile.mkstemp(suffix=".new", prefix=base_name, |
| dir=dir_name) |
| try: |
| try: |
| if uid != -1 or gid != -1: |
| os.chown(new_name, uid, gid) |
| if mode: |
| os.chmod(new_name, mode) |
| if callable(prewrite): |
| prewrite(fd) |
| if data is not None: |
| if isinstance(data, unicode): |
| data = data.encode() |
| assert isinstance(data, str) |
| to_write = len(data) |
| offset = 0 |
| while offset < to_write: |
| written = os.write(fd, buffer(data, offset)) |
| assert written >= 0 |
| assert written <= to_write - offset |
| offset += written |
| assert offset == to_write |
| 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)) |
| finally: |
| # Close file unless the file descriptor should be returned |
| if close: |
| os.close(fd) |
| else: |
| result = fd |
| |
| # Rename file to destination name |
| if not dry_run: |
| os.rename(new_name, file_name) |
| # Successful, no need to remove anymore |
| do_remove = False |
| finally: |
| if do_remove: |
| RemoveFile(new_name) |
| |
| return result |
| |
| |
| def GetFileID(path=None, fd=None): |
| """Returns the file 'id', i.e. the dev/inode and mtime information. |
| |
| Either the path to the file or the fd must be given. |
| |
| @param path: the file path |
| @param fd: a file descriptor |
| @return: a tuple of (device number, inode number, mtime) |
| |
| """ |
| if [path, fd].count(None) != 1: |
| raise errors.ProgrammerError("One and only one of fd/path must be given") |
| |
| if fd is None: |
| st = os.stat(path) |
| else: |
| st = os.fstat(fd) |
| |
| return (st.st_dev, st.st_ino, st.st_mtime) |
| |
| |
| def VerifyFileID(fi_disk, fi_ours): |
| """Verifies that two file IDs are matching. |
| |
| Differences in the inode/device are not accepted, but and older |
| timestamp for fi_disk is accepted. |
| |
| @param fi_disk: tuple (dev, inode, mtime) representing the actual |
| file data |
| @param fi_ours: tuple (dev, inode, mtime) representing the last |
| written file data |
| @rtype: boolean |
| |
| """ |
| (d1, i1, m1) = fi_disk |
| (d2, i2, m2) = fi_ours |
| |
| return (d1, i1) == (d2, i2) and m1 <= m2 |
| |
| |
| def SafeWriteFile(file_name, file_id, **kwargs): |
| """Wraper over L{WriteFile} that locks the target file. |
| |
| By keeping the target file locked during WriteFile, we ensure that |
| cooperating writers will safely serialise access to the file. |
| |
| @type file_name: str |
| @param file_name: the target filename |
| @type file_id: tuple |
| @param file_id: a result from L{GetFileID} |
| |
| """ |
| fd = os.open(file_name, os.O_RDONLY | os.O_CREAT) |
| try: |
| filelock.LockFile(fd) |
| if file_id is not None: |
| disk_id = GetFileID(fd=fd) |
| if not VerifyFileID(disk_id, file_id): |
| raise errors.LockError("Cannot overwrite file %s, it has been modified" |
| " since last written" % file_name) |
| return WriteFile(file_name, **kwargs) |
| finally: |
| os.close(fd) |
| |
| |
| 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 RemoveFile(filename): |
| """Remove a file ignoring some errors. |
| |
| Remove a file, ignoring non-existing ones or directories. Other |
| errors are passed. |
| |
| @type filename: str |
| @param filename: the file to be removed |
| |
| """ |
| try: |
| os.unlink(filename) |
| except OSError, err: |
| if err.errno not in (errno.ENOENT, errno.EISDIR): |
| 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, 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 |
| @param new: New path |
| @type mkdir: bool |
| @param mkdir: Whether to create target directory if it doesn't exist |
| @type mkdir_mode: int |
| @param mkdir_mode: Mode for newly created directories |
| @type dir_uid: int |
| @param dir_uid: The uid for the (if fresh created) dir |
| @type dir_gid: int |
| @param dir_gid: The gid for the (if fresh created) dir |
| |
| """ |
| try: |
| return os.rename(old, new) |
| except OSError, err: |
| # In at least one use case of this function, the job queue, directory |
| # creation is very rare. Checking for the directory before renaming is not |
| # as efficient. |
| if mkdir and err.errno == errno.ENOENT: |
| # Create directory and try again |
| dir_path = os.path.dirname(new) |
| 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. |
| |
| This is a wrapper around C{os.makedirs} adding error handling not implemented |
| before Python 2.5. |
| |
| """ |
| try: |
| os.makedirs(path, mode) |
| except OSError, err: |
| # Ignore EEXIST. This is only handled in os.makedirs as included in |
| # Python 2.5 and above. |
| if err.errno != errno.EEXIST or not os.path.exists(path): |
| raise |
| |
| |
| def TimestampForFilename(): |
| """Returns the current time formatted for filenames. |
| |
| The format doesn't contain colons as some shells and applications treat them |
| as separators. Uses the local timezone. |
| |
| """ |
| return time.strftime("%Y-%m-%d_%H_%M_%S") |
| |
| |
| def CreateBackup(file_name): |
| """Creates a backup of a file. |
| |
| @type file_name: str |
| @param file_name: file to be backed up |
| @rtype: str |
| @return: the path to the newly created backup |
| @raise errors.ProgrammerError: for invalid file names |
| |
| """ |
| if not os.path.isfile(file_name): |
| raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" % |
| file_name) |
| |
| prefix = ("%s.backup-%s." % |
| (os.path.basename(file_name), TimestampForFilename())) |
| dir_name = os.path.dirname(file_name) |
| |
| fsrc = open(file_name, "rb") |
| try: |
| (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name) |
| fdst = os.fdopen(fd, "wb") |
| try: |
| logging.debug("Backing up %s at %s", file_name, backup_name) |
| shutil.copyfileobj(fsrc, fdst) |
| finally: |
| fdst.close() |
| finally: |
| fsrc.close() |
| |
| return backup_name |
| |
| |
| def ListVisibleFiles(path, _is_mountpoint=os.path.ismount): |
| """Returns a list of visible files in a directory. |
| |
| @type path: str |
| @param path: the directory to enumerate |
| @rtype: list |
| @return: the list of all files not starting with a dot |
| @raise ProgrammerError: if L{path} is not an absolue and normalized path |
| |
| """ |
| if not IsNormAbsPath(path): |
| raise errors.ProgrammerError("Path passed to ListVisibleFiles is not" |
| " absolute/normalized: '%s'" % path) |
| |
| mountpoint = _is_mountpoint(path) |
| |
| def fn(name): |
| """File name filter. |
| |
| Ignores files starting with a dot (".") as by Unix convention they're |
| considered hidden. The "lost+found" directory found at the root of some |
| filesystems is also hidden. |
| |
| """ |
| return not (name.startswith(".") or |
| (mountpoint and name == _LOST_AND_FOUND and |
| os.path.isdir(os.path.join(path, name)))) |
| |
| return filter(fn, os.listdir(path)) |
| |
| |
| def EnsureDirs(dirs): |
| """Make required directories, if they don't exist. |
| |
| @param dirs: list of tuples (dir_name, dir_mode) |
| @type dirs: list of (string, integer) |
| |
| """ |
| for dir_name, dir_mode in dirs: |
| try: |
| os.mkdir(dir_name, dir_mode) |
| except EnvironmentError, err: |
| 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) |
| |
| |
| def FindFile(name, search_path, test=os.path.exists): |
| """Look for a filesystem object in a given path. |
| |
| This is an abstract method to search for filesystem object (files, |
| dirs) under a given search path. |
| |
| @type name: str |
| @param name: the name to look for |
| @type search_path: str |
| @param search_path: location to start at |
| @type test: callable |
| @param test: a function taking one argument that should return True |
| if the a given object is valid; the default value is |
| os.path.exists, causing only existing files to be returned |
| @rtype: str or None |
| @return: full path to the object if found, None otherwise |
| |
| """ |
| # validate the filename mask |
| if constants.EXT_PLUGIN_MASK.match(name) is None: |
| logging.critical("Invalid value passed for external script name: '%s'", |
| name) |
| return None |
| |
| for dir_name in search_path: |
| # FIXME: investigate switch to PathJoin |
| item_name = os.path.sep.join([dir_name, name]) |
| # check the user test and that we're indeed resolving to the given |
| # basename |
| if test(item_name) and os.path.basename(item_name) == name: |
| return item_name |
| return None |
| |
| |
| def IsNormAbsPath(path): |
| """Check whether a path is absolute and also normalized |
| |
| This avoids things like /dir/../../other/path to be valid. |
| |
| """ |
| 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 comparison 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)) |
| |
| norm_other = os.path.normpath(other_path) |
| |
| if norm_other == os.sep: |
| # The root directory can never be below another path |
| return False |
| |
| norm_root = os.path.normpath(root) |
| |
| if norm_root == os.sep: |
| # This is the root directory, no need to add another slash |
| prepared_root = norm_root |
| else: |
| prepared_root = "%s%s" % (norm_root, os.sep) |
| |
| return os.path.commonprefix([prepared_root, norm_other]) == prepared_root |
| |
| |
| def PathJoin(*args): |
| """Safe-join a list of path components. |
| |
| Requirements: |
| - the first argument must be an absolute path |
| - no component in the path must have backtracking (e.g. /../), |
| since we check for normalization at the end |
| |
| @param args: the path components to be joined |
| @raise ValueError: for invalid paths |
| |
| """ |
| # ensure we're having at least one path passed in |
| assert args |
| # ensure the first component is an absolute and normalized path name |
| root = args[0] |
| if not IsNormAbsPath(root): |
| raise ValueError("Invalid parameter to PathJoin: '%s'" % str(args[0])) |
| result = os.path.join(*args) |
| # ensure that the whole path is normalized |
| if not IsNormAbsPath(result): |
| raise ValueError("Invalid parameters to PathJoin: '%s'" % str(args)) |
| # check that we're still under the original prefix |
| if not IsBelowDir(root, result): |
| raise ValueError("Error: path joining resulted in different prefix" |
| " (%s != %s)" % (result, root)) |
| return result |
| |
| |
| def TailFile(fname, lines=20): |
| """Return the last lines from a file. |
| |
| @note: this function will only read and parse the last 4KB of |
| the file; if the lines are very long, it could be that less |
| than the requested number of lines are returned |
| |
| @param fname: the file name |
| @type lines: int |
| @param lines: the (maximum) number of lines to return |
| |
| """ |
| fd = open(fname, "r") |
| try: |
| fd.seek(0, 2) |
| pos = fd.tell() |
| pos = max(0, pos - 4096) |
| fd.seek(pos, 0) |
| raw_data = fd.read() |
| finally: |
| fd.close() |
| |
| rows = raw_data.splitlines() |
| return rows[-lines:] |
| |
| |
| def BytesToMebibyte(value): |
| """Converts bytes to mebibytes. |
| |
| @type value: int |
| @param value: Value in bytes |
| @rtype: int |
| @return: Value in mebibytes |
| |
| """ |
| return int(round(value / (1024.0 * 1024.0), 0)) |
| |
| |
| def CalculateDirectorySize(path): |
| """Calculates the size of a directory recursively. |
| |
| @type path: string |
| @param path: Path to directory |
| @rtype: int |
| @return: Size in mebibytes |
| |
| """ |
| size = 0 |
| |
| for (curpath, _, files) in os.walk(path): |
| for filename in files: |
| st = os.lstat(PathJoin(curpath, filename)) |
| size += st.st_size |
| |
| return BytesToMebibyte(size) |
| |
| |
| def GetFilesystemStats(path): |
| """Returns the total and free space on a filesystem. |
| |
| @type path: string |
| @param path: Path on filesystem to be examined |
| @rtype: int |
| @return: tuple of (Total space, Free space) in mebibytes |
| |
| """ |
| st = os.statvfs(path) |
| |
| fsize = BytesToMebibyte(st.f_bavail * st.f_frsize) |
| tsize = BytesToMebibyte(st.f_blocks * st.f_frsize) |
| return (tsize, fsize) |
| |
| |
| def ReadPidFile(pidfile): |
| """Read a pid from a file. |
| |
| @type pidfile: string |
| @param pidfile: path to the file containing the pid |
| @rtype: int |
| @return: The process id, if the file exists and contains a valid PID, |
| otherwise 0 |
| |
| """ |
| try: |
| raw_data = ReadOneLineFile(pidfile) |
| except EnvironmentError, err: |
| if err.errno != errno.ENOENT: |
| logging.exception("Can't read pid file") |
| return 0 |
| |
| return _ParsePidFileContents(raw_data) |
| |
| |
| def _ParsePidFileContents(data): |
| """Tries to extract a process ID from a PID file's content. |
| |
| @type data: string |
| @rtype: int |
| @return: Zero if nothing could be read, PID otherwise |
| |
| """ |
| try: |
| pid = int(data) |
| except (TypeError, ValueError): |
| logging.info("Can't parse pid file contents", exc_info=True) |
| return 0 |
| else: |
| return pid |
| |
| |
| def ReadLockedPidFile(path): |
| """Reads a locked PID file. |
| |
| This can be used together with L{utils.process.StartDaemon}. |
| |
| @type path: string |
| @param path: Path to PID file |
| @return: PID as integer or, if file was unlocked or couldn't be opened, None |
| |
| """ |
| try: |
| fd = os.open(path, os.O_RDONLY) |
| except EnvironmentError, err: |
| if err.errno == errno.ENOENT: |
| # PID file doesn't exist |
| return None |
| raise |
| |
| try: |
| try: |
| # Try to acquire lock |
| filelock.LockFile(fd) |
| except errors.LockError: |
| # Couldn't lock, daemon is running |
| return int(os.read(fd, 100)) |
| finally: |
| os.close(fd) |
| |
| return None |
| |
| |
| def _SplitSshKey(key): |
| """Splits a line for SSH's C{authorized_keys} file. |
| |
| If the line has no options (e.g. no C{command="..."}), only the significant |
| parts, the key type and its hash, are used. Otherwise the whole line is used |
| (split at whitespace). |
| |
| @type key: string |
| @param key: Key line |
| @rtype: tuple |
| |
| """ |
| parts = key.split() |
| |
| if parts and parts[0] in constants.SSHAK_ALL: |
| # If the key has no options in front of it, we only want the significant |
| # fields |
| return (False, parts[:2]) |
| else: |
| # Can't properly split the line, so use everything |
| return (True, parts) |
| |
| |
| def AddAuthorizedKey(file_obj, key): |
| """Adds an SSH public key to an 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 = _SplitSshKey(key) |
| |
| if isinstance(file_obj, basestring): |
| f = open(file_obj, "a+") |
| else: |
| f = file_obj |
| |
| try: |
| nl = True |
| for line in f: |
| # Ignore whitespace changes |
| if _SplitSshKey(line) == key_fields: |
| break |
| nl = line.endswith("\n") |
| else: |
| if not nl: |
| f.write("\n") |
| f.write(key.rstrip("\r\n")) |
| f.write("\n") |
| f.flush() |
| finally: |
| f.close() |
| |
| |
| def RemoveAuthorizedKey(file_name, key): |
| """Removes an SSH public key from an authorized_keys file. |
| |
| @type file_name: str |
| @param file_name: path to authorized_keys file |
| @type key: str |
| @param key: string containing key |
| |
| """ |
| key_fields = _SplitSshKey(key) |
| |
| fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name)) |
| try: |
| out = os.fdopen(fd, "w") |
| try: |
| f = open(file_name, "r") |
| try: |
| for line in f: |
| # Ignore whitespace changes while comparing lines |
| if _SplitSshKey(line) != key_fields: |
| out.write(line) |
| |
| out.flush() |
| os.rename(tmpname, file_name) |
| finally: |
| f.close() |
| finally: |
| out.close() |
| except: |
| RemoveFile(tmpname) |
| raise |
| |
| |
| def DaemonPidFileName(name): |
| """Compute a ganeti pid file absolute path |
| |
| @type name: str |
| @param name: the daemon name |
| @rtype: str |
| @return: the full path to the pidfile corresponding to the given |
| daemon name |
| |
| """ |
| return PathJoin(pathutils.RUN_DIR, "%s.pid" % name) |
| |
| |
| def WritePidFile(pidfile): |
| """Write the current process pidfile. |
| |
| @type pidfile: string |
| @param pidfile: the path to the file to be written |
| @raise errors.LockError: if the pid file already exists and |
| points to a live process |
| @rtype: int |
| @return: the file descriptor of the lock file; do not close this unless |
| you want to unlock the pid file |
| |
| """ |
| # We don't rename nor truncate the file to not drop locks under |
| # existing processes |
| fd_pidfile = os.open(pidfile, os.O_RDWR | os.O_CREAT, 0600) |
| |
| # Lock the PID file (and fail if not possible to do so). Any code |
| # wanting to send a signal to the daemon should try to lock the PID |
| # file before reading it. If acquiring the lock succeeds, the daemon is |
| # no longer running and the signal should not be sent. |
| try: |
| filelock.LockFile(fd_pidfile) |
| except errors.LockError: |
| msg = ["PID file '%s' is already locked by another process" % pidfile] |
| # Try to read PID file |
| pid = _ParsePidFileContents(os.read(fd_pidfile, 100)) |
| if pid > 0: |
| msg.append(", PID read from file is %s" % pid) |
| raise errors.PidFileLockError("".join(msg)) |
| |
| os.write(fd_pidfile, "%d\n" % os.getpid()) |
| |
| return fd_pidfile |
| |
| |
| def ReadWatcherPauseFile(filename, now=None, remove_after=3600): |
| """Reads the watcher pause file. |
| |
| @type filename: string |
| @param filename: Path to watcher pause file |
| @type now: None, float or int |
| @param now: Current time as Unix timestamp |
| @type remove_after: int |
| @param remove_after: Remove watcher pause file after specified amount of |
| seconds past the pause end time |
| |
| """ |
| if now is None: |
| now = time.time() |
| |
| try: |
| value = ReadFile(filename) |
| except IOError, err: |
| if err.errno != errno.ENOENT: |
| raise |
| value = None |
| |
| if value is not None: |
| try: |
| value = int(value) |
| except ValueError: |
| logging.warning(("Watcher pause file (%s) contains invalid value," |
| " removing it"), filename) |
| RemoveFile(filename) |
| value = None |
| |
| if value is not None: |
| # Remove file if it's outdated |
| if now > (value + remove_after): |
| RemoveFile(filename) |
| value = None |
| |
| elif now > value: |
| value = None |
| |
| return value |
| |
| |
| def NewUUID(): |
| """Returns a random UUID. |
| |
| @note: This is a Linux-specific method as it uses the /proc |
| filesystem. |
| @rtype: str |
| |
| """ |
| return ReadFile(constants.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()) |
| |
| |
| def IsUserInGroup(uid, gid): |
| """Returns True if the user belongs to the group. |
| |
| @type uid: int |
| @param uid: the user id |
| @type gid: int |
| @param gid: the group id |
| @rtype: bool |
| |
| """ |
| user = pwd.getpwuid(uid) |
| group = grp.getgrgid(gid) |
| return user.pw_gid == gid or user.pw_name in group.gr_mem |
| |
| |
| def CanRead(username, filename): |
| """Returns True if the user can access (read) the file. |
| |
| @type username: string |
| @param username: the name of the user |
| @type filename: string |
| @param filename: the name of the file |
| @rtype: bool |
| |
| """ |
| filestats = os.stat(filename) |
| user = pwd.getpwnam(username) |
| uid = user.pw_uid |
| user_readable = filestats.st_mode & stat.S_IRUSR != 0 |
| group_readable = filestats.st_mode & stat.S_IRGRP != 0 |
| return ((filestats.st_uid == uid and user_readable) |
| or (filestats.st_uid != uid and |
| IsUserInGroup(uid, filestats.st_gid) and group_readable)) |