# Copyright (c) 2012-2013 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import os import errno import stat import random import time import xattr from collections import defaultdict from itertools import repeat import ctypes from eventlet import sleep from swift.common.utils import load_libc_function from gluster.swift.common.exceptions import FileOrDirNotFoundError, \ NotDirectoryError, GlusterFileSystemOSError from swift.common.exceptions import DiskFileNoSpace def do_exists(path): return os.path.exists(path) def do_touch(path): with open(path, 'a'): os.utime(path, None) def do_getctime(path): return os.path.getctime(path) def do_getmtime(path): return os.path.getmtime(path) def do_isdir(path): return os.path.isdir(path) def do_getsize(path): return os.path.getsize(path) def do_getxattr(path, key): return xattr.getxattr(path, key) def do_setxattr(path, key, value): xattr.setxattr(path, key, value) def do_removexattr(path, key): xattr.removexattr(path, key) def do_walk(*args, **kwargs): return os.walk(*args, **kwargs) def do_write(fd, buf): try: cnt = os.write(fd, buf) except OSError as err: filename = get_filename_from_fd(fd) if err.errno in (errno.ENOSPC, errno.EDQUOT): do_log_rl("do_write(%d, msg[%d]) failed: %s : %s", fd, len(buf), err, filename) raise DiskFileNoSpace() else: raise GlusterFileSystemOSError( err.errno, '%s, os.write("%s", ...)' % (err.strerror, fd)) return cnt def do_read(fd, n): try: buf = os.read(fd, n) except OSError as err: raise GlusterFileSystemOSError( err.errno, '%s, os.write("%s", ...)' % (err.strerror, fd)) return buf def do_ismount(path): """ Test whether a path is a mount point. This is code hijacked from C Python 2.6.8, adapted to remove the extra lstat() system call. """ try: s1 = os.lstat(path) except os.error as err: if err.errno == errno.ENOENT: # It doesn't exist -- so not a mount point :-) return False else: raise GlusterFileSystemOSError( err.errno, '%s, os.lstat("%s")' % (err.strerror, path)) if stat.S_ISLNK(s1.st_mode): # A symlink can never be a mount point return False try: s2 = os.lstat(os.path.join(path, '..')) except os.error as err: raise GlusterFileSystemOSError( err.errno, '%s, os.lstat("%s")' % (err.strerror, os.path.join(path, '..'))) dev1 = s1.st_dev dev2 = s2.st_dev if dev1 != dev2: # path/.. on a different device as path return True ino1 = s1.st_ino ino2 = s2.st_ino if ino1 == ino2: # path/.. is the same i-node as path return True return False def do_mkdir(path): os.mkdir(path) def do_listdir(path): try: buf = os.listdir(path) except OSError as err: raise GlusterFileSystemOSError( err.errno, '%s, os.listdir("%s")' % (err.strerror, path)) return buf def dir_empty(path): """ Return true if directory is empty (or does not exist), false otherwise. :param path: Directory path :returns: True/False """ try: files = do_listdir(path) return not files except GlusterFileSystemOSError as err: if err.errno in (errno.ENOENT, errno.ESTALE): raise FileOrDirNotFoundError() if err.errno == errno.ENOTDIR: raise NotDirectoryError() raise def do_rmdir(path): try: os.rmdir(path) except OSError as err: raise GlusterFileSystemOSError( err.errno, '%s, os.rmdir("%s")' % (err.strerror, path)) def do_chown(path, uid, gid): try: os.chown(path, uid, gid) except OSError as err: raise GlusterFileSystemOSError( err.errno, '%s, os.chown("%s", %s, %s)' % ( err.strerror, path, uid, gid)) def do_fchown(fd, uid, gid): try: os.fchown(fd, uid, gid) except OSError as err: raise GlusterFileSystemOSError( err.errno, '%s, os.fchown(%s, %s, %s)' % ( err.strerror, fd, uid, gid)) _STAT_ATTEMPTS = 10 def do_stat(path): serr = None for i in range(0, _STAT_ATTEMPTS): try: stats = os.stat(path) except OSError as err: if err.errno == errno.EIO: # Retry EIO assuming it is a transient error from FUSE after a # short random sleep serr = err sleep(random.uniform(0.001, 0.005)) continue if err.errno in (errno.ENOENT, errno.ESTALE): stats = None else: raise GlusterFileSystemOSError( err.errno, '%s, os.stat("%s")[%d attempts]' % ( err.strerror, path, i)) if i > 0: logging.warn("fs_utils.do_stat():" " os.stat('%s') retried %d times (%s)", path, i, 'success' if stats else 'failure') return stats else: raise GlusterFileSystemOSError( serr.errno, '%s, os.stat("%s")[%d attempts]' % ( serr.strerror, path, _STAT_ATTEMPTS)) def do_fstat(fd): try: stats = os.fstat(fd) except OSError as err: raise GlusterFileSystemOSError( err.errno, '%s, os.fstat(%s)' % (err.strerror, fd)) return stats def do_open(path, flags, **kwargs): try: fd = os.open(path, flags, **kwargs) except OSError as err: raise GlusterFileSystemOSError( err.errno, '%s, os.open("%s", %x, %r)' % ( err.strerror, path, flags, kwargs)) return fd def do_dup(fd): return os.dup(fd) def do_close(fd): try: os.close(fd) except OSError as err: if err.errno in (errno.ENOSPC, errno.EDQUOT): filename = get_filename_from_fd(fd) do_log_rl("do_close(%d) failed: %s : %s", fd, err, filename) raise DiskFileNoSpace() else: raise GlusterFileSystemOSError( err.errno, '%s, os.close(%s)' % (err.strerror, fd)) def do_unlink(path, log=True): try: os.unlink(path) except OSError as err: if err.errno not in (errno.ENOENT, errno.ESTALE): raise GlusterFileSystemOSError( err.errno, '%s, os.unlink("%s")' % (err.strerror, path)) else: logging.warn("fs_utils: os.unlink failed on non-existent path: %s", path) def do_rename(old_path, new_path): try: os.rename(old_path, new_path) except OSError as err: raise GlusterFileSystemOSError( err.errno, '%s, os.rename("%s", "%s")' % ( err.strerror, old_path, new_path)) def do_fsync(fd): try: os.fsync(fd) except OSError as err: raise GlusterFileSystemOSError( err.errno, '%s, os.fsync("%s")' % (err.strerror, fd)) def do_fdatasync(fd): try: os.fdatasync(fd) except AttributeError: do_fsync(fd) except OSError as err: raise GlusterFileSystemOSError( err.errno, '%s, os.fsync("%s")' % (err.strerror, fd)) _posix_fadvise = None def do_fadvise64(fd, offset, length): global _posix_fadvise if _posix_fadvise is None: _posix_fadvise = load_libc_function('posix_fadvise64') # 4 means "POSIX_FADV_DONTNEED" _posix_fadvise(fd, ctypes.c_uint64(offset), ctypes.c_uint64(length), 4) def do_lseek(fd, pos, how): try: os.lseek(fd, pos, how) except OSError as err: raise GlusterFileSystemOSError( err.errno, '%s, os.fsync("%s")' % (err.strerror, fd)) def mkdirs(path): """ Ensures the path is a directory or makes it if not. Errors if the path exists but is a file or on permissions failure. :param path: path to create """ try: os.makedirs(path) except OSError as err: if err.errno == errno.EEXIST and os.path.isdir(path): return elif err.errno in (errno.ENOSPC, errno.EDQUOT): do_log_rl("mkdirs(%s) failed: %s", path, err) raise DiskFileNoSpace() else: raise GlusterFileSystemOSError( err.errno, '%s, os.makedirs("%s")' % (err.strerror, path)) def get_filename_from_fd(fd, verify=False): """ Given the file descriptor, this method attempts to get the filename as it was when opened. This may not give accurate results in following cases: - file was renamed/moved/deleted after it was opened - file has multiple hardlinks :param fd: file descriptor of file opened :param verify: If True, performs additional checks using inode number """ filename = None if isinstance(fd, int): try: filename = os.readlink("/proc/self/fd/" + str(fd)) except OSError: pass if not verify: return filename # If verify = True, we compare st_dev and st_ino of file and fd. # This involves additional stat and fstat calls. So this is disabled # by default. if filename and fd: s_file = do_stat(filename) s_fd = do_fstat(fd) if s_file and s_fd: if (s_file.st_ino, s_file.st_dev) == (s_fd.st_ino, s_fd.st_dev): return filename return None def static_var(varname, value): """Decorator function to create pseudo static variables.""" def decorate(func): setattr(func, varname, value) return func return decorate # Rate limit to emit log message once a second _DO_LOG_RL_INTERVAL = 1.0 @static_var("counter", defaultdict(int)) @static_var("last_called", defaultdict(repeat(0.0).next)) def do_log_rl(msg, *args, **kwargs): """ Rate limited logger. :param msg: String or message to be logged :param log_level: Possible values- error, warning, info, debug, critical """ log_level = kwargs.get('log_level', "error") if log_level not in ("error", "warning", "info", "debug", "critical"): log_level = "error" do_log_rl.counter[msg] += 1 # Increment msg counter interval = time.time() - do_log_rl.last_called[msg] if interval >= _DO_LOG_RL_INTERVAL: # Prefix PID of process and message count to original log msg emit_msg = "[PID:" + str(os.getpid()) + "]" \ + "[RateLimitedLog;Count:" + str(do_log_rl.counter[msg]) + "] " \ + msg # log_level is a param for do_log_rl and not for logging.* methods try: del kwargs['log_level'] except KeyError: pass getattr(logging, log_level)(emit_msg, *args, **kwargs) # Emit msg do_log_rl.counter[msg] = 0 # Reset msg counter when message is emitted do_log_rl.last_called[msg] = time.time() # Reset msg time