4 # Copyright (C) 2006, 2007, 2010, 2011, 2012 Google Inc.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21 """Utility functions for I/O.
35 from ganeti import errors
36 from ganeti import constants
37 from ganeti import pathutils
38 from ganeti.utils import filelock
40 #: Directory used by fsck(8) to store recovered data, usually at a file
41 #: system's root directory
42 _LOST_AND_FOUND = "lost+found"
44 # Possible values for keep_perms in WriteFile()
57 """Format an EnvironmentError exception.
59 If the L{err} argument has an errno attribute, it will be looked up
60 and converted into a textual C{E...} description. Otherwise the
61 string representation of the error will be returned.
63 @type err: L{EnvironmentError}
64 @param err: the exception to format
67 if hasattr(err, "errno"):
68 detail = errno.errorcode[err.errno]
75 """Helper to store file handle's C{fstat}.
77 Useful in combination with L{ReadFile}'s C{preread} parameter.
81 """Initializes this class.
86 def __call__(self, fh):
87 """Calls C{fstat} on file handle.
90 self.st = os.fstat(fh.fileno())
93 def ReadFile(file_name, size=-1, preread=None):
97 @param size: Read at most size bytes (if negative, entire file)
98 @type preread: callable receiving file handle as single parameter
99 @param preread: Function called before file is read
101 @return: the (possibly partial) content of the file
104 f = open(file_name, "r")
114 def WriteFile(file_name, fn=None, data=None,
115 mode=None, uid=-1, gid=-1,
116 atime=None, mtime=None, close=True,
117 dry_run=False, backup=False,
118 prewrite=None, postwrite=None, keep_perms=KP_NEVER):
119 """(Over)write a file atomically.
121 The file_name and either fn (a function taking one argument, the
122 file descriptor, and which should write the data to it) or data (the
123 contents of the file) must be passed. The other arguments are
124 optional and allow setting the file mode, owner and group, and the
125 mtime/atime of the file.
127 If the function doesn't raise an exception, it has succeeded and the
128 target file has the new contents. If the function has raised an
129 exception, an existing target file should be unmodified and the
130 temporary file should be removed.
133 @param file_name: the target filename
135 @param fn: content writing function, called with
136 file descriptor as parameter
138 @param data: contents of the file
140 @param mode: file mode
142 @param uid: the owner of the file
144 @param gid: the group of the file
146 @param atime: a custom access time to be set on the file
148 @param mtime: a custom modification time to be set on the file
150 @param close: whether to close file after writing it
151 @type prewrite: callable
152 @param prewrite: function to be called before writing content
153 @type postwrite: callable
154 @param postwrite: function to be called after writing content
155 @type keep_perms: members of L{KEEP_PERMS_VALUES}
156 @param keep_perms: if L{KP_NEVER} (default), owner, group, and mode are
157 taken from the other parameters; if L{KP_ALWAYS}, owner, group, and
158 mode are copied from the existing file; if L{KP_IF_EXISTS}, owner,
159 group, and mode are taken from the file, and if the file doesn't
160 exist, they are taken from the other parameters. It is an error to
161 pass L{KP_ALWAYS} when the file doesn't exist or when C{uid}, C{gid},
162 or C{mode} are set to non-default values.
165 @return: None if the 'close' parameter evaluates to True,
166 otherwise the file descriptor
168 @raise errors.ProgrammerError: if any of the arguments are not valid
171 if not os.path.isabs(file_name):
172 raise errors.ProgrammerError("Path passed to WriteFile is not"
173 " absolute: '%s'" % file_name)
175 if [fn, data].count(None) != 1:
176 raise errors.ProgrammerError("fn or data required")
178 if [atime, mtime].count(None) == 1:
179 raise errors.ProgrammerError("Both atime and mtime must be either"
182 if not keep_perms in KEEP_PERMS_VALUES:
183 raise errors.ProgrammerError("Invalid value for keep_perms: %s" %
185 if keep_perms == KP_ALWAYS and (uid != -1 or gid != -1 or mode is not None):
186 raise errors.ProgrammerError("When keep_perms==KP_ALWAYS, 'uid', 'gid',"
187 " and 'mode' cannot be set")
189 if backup and not dry_run and os.path.isfile(file_name):
190 CreateBackup(file_name)
192 if keep_perms == KP_ALWAYS or keep_perms == KP_IF_EXISTS:
193 # os.stat() raises an exception if the file doesn't exist
195 file_stat = os.stat(file_name)
196 mode = stat.S_IMODE(file_stat.st_mode)
197 uid = file_stat.st_uid
198 gid = file_stat.st_gid
200 if keep_perms == KP_ALWAYS:
202 # else: if keeep_perms == KP_IF_EXISTS it's ok if the file doesn't exist
204 # Whether temporary file needs to be removed (e.g. if any error occurs)
210 (dir_name, base_name) = os.path.split(file_name)
211 (fd, new_name) = tempfile.mkstemp(suffix=".new", prefix=base_name,
215 if uid != -1 or gid != -1:
216 os.chown(new_name, uid, gid)
218 os.chmod(new_name, mode)
219 if callable(prewrite):
222 if isinstance(data, unicode):
224 assert isinstance(data, str)
227 while offset < to_write:
228 written = os.write(fd, buffer(data, offset))
230 assert written <= to_write - offset
232 assert offset == to_write
235 if callable(postwrite):
238 if atime is not None and mtime is not None:
239 os.utime(new_name, (atime, mtime))
241 # Close file unless the file descriptor should be returned
247 # Rename file to destination name
249 os.rename(new_name, file_name)
250 # Successful, no need to remove anymore
259 def GetFileID(path=None, fd=None):
260 """Returns the file 'id', i.e. the dev/inode and mtime information.
262 Either the path to the file or the fd must be given.
264 @param path: the file path
265 @param fd: a file descriptor
266 @return: a tuple of (device number, inode number, mtime)
269 if [path, fd].count(None) != 1:
270 raise errors.ProgrammerError("One and only one of fd/path must be given")
277 return (st.st_dev, st.st_ino, st.st_mtime)
280 def VerifyFileID(fi_disk, fi_ours):
281 """Verifies that two file IDs are matching.
283 Differences in the inode/device are not accepted, but and older
284 timestamp for fi_disk is accepted.
286 @param fi_disk: tuple (dev, inode, mtime) representing the actual
288 @param fi_ours: tuple (dev, inode, mtime) representing the last
293 (d1, i1, m1) = fi_disk
294 (d2, i2, m2) = fi_ours
296 return (d1, i1) == (d2, i2) and m1 <= m2
299 def SafeWriteFile(file_name, file_id, **kwargs):
300 """Wraper over L{WriteFile} that locks the target file.
302 By keeping the target file locked during WriteFile, we ensure that
303 cooperating writers will safely serialise access to the file.
306 @param file_name: the target filename
308 @param file_id: a result from L{GetFileID}
311 fd = os.open(file_name, os.O_RDONLY | os.O_CREAT)
313 filelock.LockFile(fd)
314 if file_id is not None:
315 disk_id = GetFileID(fd=fd)
316 if not VerifyFileID(disk_id, file_id):
317 raise errors.LockError("Cannot overwrite file %s, it has been modified"
318 " since last written" % file_name)
319 return WriteFile(file_name, **kwargs)
324 def ReadOneLineFile(file_name, strict=False):
325 """Return the first non-empty line from a file.
327 @type strict: boolean
328 @param strict: if True, abort if the file has more than one
332 file_lines = ReadFile(file_name).splitlines()
333 full_lines = filter(bool, file_lines)
334 if not file_lines or not full_lines:
335 raise errors.GenericError("No data in one-liner file %s" % file_name)
336 elif strict and len(full_lines) > 1:
337 raise errors.GenericError("Too many lines in one-liner file %s" %
342 def RemoveFile(filename):
343 """Remove a file ignoring some errors.
345 Remove a file, ignoring non-existing ones or directories. Other
349 @param filename: the file to be removed
355 if err.errno not in (errno.ENOENT, errno.EISDIR):
359 def RemoveDir(dirname):
360 """Remove an empty directory.
362 Remove a directory, ignoring non-existing ones.
363 Other errors are passed. This includes the case,
364 where the directory is not empty, so it can't be removed.
367 @param dirname: the empty directory to be removed
373 if err.errno != errno.ENOENT:
377 def RenameFile(old, new, mkdir=False, mkdir_mode=0750, dir_uid=None,
381 This just creates the very least directory if it does not exist and C{mkdir}
385 @param old: Original path
389 @param mkdir: Whether to create target directory if it doesn't exist
390 @type mkdir_mode: int
391 @param mkdir_mode: Mode for newly created directories
393 @param dir_uid: The uid for the (if fresh created) dir
395 @param dir_gid: The gid for the (if fresh created) dir
399 return os.rename(old, new)
401 # In at least one use case of this function, the job queue, directory
402 # creation is very rare. Checking for the directory before renaming is not
404 if mkdir and err.errno == errno.ENOENT:
405 # Create directory and try again
406 dir_path = os.path.dirname(new)
407 MakeDirWithPerm(dir_path, mkdir_mode, dir_uid, dir_gid)
409 return os.rename(old, new)
414 def EnforcePermission(path, mode, uid=None, gid=None, must_exist=True,
415 _chmod_fn=os.chmod, _chown_fn=os.chown, _stat_fn=os.stat):
416 """Enforces that given path has given permissions.
418 @param path: The path to the file
419 @param mode: The mode of the file
420 @param uid: The uid of the owner of this file
421 @param gid: The gid of the owner of this file
422 @param must_exist: Specifies if non-existance of path will be an error
423 @param _chmod_fn: chmod function to use (unittest only)
424 @param _chown_fn: chown function to use (unittest only)
427 logging.debug("Checking %s", path)
429 # chown takes -1 if you want to keep one part of the ownership, however
430 # None is Python standard for that. So we remap them here.
439 fmode = stat.S_IMODE(st[stat.ST_MODE])
441 logging.debug("Changing mode of %s from %#o to %#o", path, fmode, mode)
442 _chmod_fn(path, mode)
444 if max(uid, gid) > -1:
445 fuid = st[stat.ST_UID]
446 fgid = st[stat.ST_GID]
447 if fuid != uid or fgid != gid:
448 logging.debug("Changing owner of %s from UID %s/GID %s to"
449 " UID %s/GID %s", path, fuid, fgid, uid, gid)
450 _chown_fn(path, uid, gid)
451 except EnvironmentError, err:
452 if err.errno == errno.ENOENT:
454 raise errors.GenericError("Path %s should exist, but does not" % path)
456 raise errors.GenericError("Error while changing permissions on %s: %s" %
460 def MakeDirWithPerm(path, mode, uid, gid, _lstat_fn=os.lstat,
461 _mkdir_fn=os.mkdir, _perm_fn=EnforcePermission):
462 """Enforces that given path is a dir and has given mode, uid and gid set.
464 @param path: The path to the file
465 @param mode: The mode of the file
466 @param uid: The uid of the owner of this file
467 @param gid: The gid of the owner of this file
468 @param _lstat_fn: Stat function to use (unittest only)
469 @param _mkdir_fn: mkdir function to use (unittest only)
470 @param _perm_fn: permission setter function to use (unittest only)
473 logging.debug("Checking directory %s", path)
475 # We don't want to follow symlinks
477 except EnvironmentError, err:
478 if err.errno != errno.ENOENT:
479 raise errors.GenericError("stat(2) on %s failed: %s" % (path, err))
482 if not stat.S_ISDIR(st[stat.ST_MODE]):
483 raise errors.GenericError(("Path %s is expected to be a directory, but "
486 _perm_fn(path, mode, uid=uid, gid=gid)
489 def Makedirs(path, mode=0750):
490 """Super-mkdir; create a leaf directory and all intermediate ones.
492 This is a wrapper around C{os.makedirs} adding error handling not implemented
497 os.makedirs(path, mode)
499 # Ignore EEXIST. This is only handled in os.makedirs as included in
500 # Python 2.5 and above.
501 if err.errno != errno.EEXIST or not os.path.exists(path):
505 def TimestampForFilename():
506 """Returns the current time formatted for filenames.
508 The format doesn't contain colons as some shells and applications treat them
509 as separators. Uses the local timezone.
512 return time.strftime("%Y-%m-%d_%H_%M_%S")
515 def CreateBackup(file_name):
516 """Creates a backup of a file.
519 @param file_name: file to be backed up
521 @return: the path to the newly created backup
522 @raise errors.ProgrammerError: for invalid file names
525 if not os.path.isfile(file_name):
526 raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
529 prefix = ("%s.backup-%s." %
530 (os.path.basename(file_name), TimestampForFilename()))
531 dir_name = os.path.dirname(file_name)
533 fsrc = open(file_name, "rb")
535 (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
536 fdst = os.fdopen(fd, "wb")
538 logging.debug("Backing up %s at %s", file_name, backup_name)
539 shutil.copyfileobj(fsrc, fdst)
548 def ListVisibleFiles(path, _is_mountpoint=os.path.ismount):
549 """Returns a list of visible files in a directory.
552 @param path: the directory to enumerate
554 @return: the list of all files not starting with a dot
555 @raise ProgrammerError: if L{path} is not an absolue and normalized path
558 if not IsNormAbsPath(path):
559 raise errors.ProgrammerError("Path passed to ListVisibleFiles is not"
560 " absolute/normalized: '%s'" % path)
562 mountpoint = _is_mountpoint(path)
567 Ignores files starting with a dot (".") as by Unix convention they're
568 considered hidden. The "lost+found" directory found at the root of some
569 filesystems is also hidden.
572 return not (name.startswith(".") or
573 (mountpoint and name == _LOST_AND_FOUND and
574 os.path.isdir(os.path.join(path, name))))
576 return filter(fn, os.listdir(path))
579 def EnsureDirs(dirs):
580 """Make required directories, if they don't exist.
582 @param dirs: list of tuples (dir_name, dir_mode)
583 @type dirs: list of (string, integer)
586 for dir_name, dir_mode in dirs:
588 os.mkdir(dir_name, dir_mode)
589 except EnvironmentError, err:
590 if err.errno != errno.EEXIST:
591 raise errors.GenericError("Cannot create needed directory"
592 " '%s': %s" % (dir_name, err))
594 os.chmod(dir_name, dir_mode)
595 except EnvironmentError, err:
596 raise errors.GenericError("Cannot change directory permissions on"
597 " '%s': %s" % (dir_name, err))
598 if not os.path.isdir(dir_name):
599 raise errors.GenericError("%s is not a directory" % dir_name)
602 def FindFile(name, search_path, test=os.path.exists):
603 """Look for a filesystem object in a given path.
605 This is an abstract method to search for filesystem object (files,
606 dirs) under a given search path.
609 @param name: the name to look for
610 @type search_path: str
611 @param search_path: location to start at
613 @param test: a function taking one argument that should return True
614 if the a given object is valid; the default value is
615 os.path.exists, causing only existing files to be returned
617 @return: full path to the object if found, None otherwise
620 # validate the filename mask
621 if constants.EXT_PLUGIN_MASK.match(name) is None:
622 logging.critical("Invalid value passed for external script name: '%s'",
626 for dir_name in search_path:
627 # FIXME: investigate switch to PathJoin
628 item_name = os.path.sep.join([dir_name, name])
629 # check the user test and that we're indeed resolving to the given
631 if test(item_name) and os.path.basename(item_name) == name:
636 def IsNormAbsPath(path):
637 """Check whether a path is absolute and also normalized
639 This avoids things like /dir/../../other/path to be valid.
642 return os.path.normpath(path) == path and os.path.isabs(path)
645 def IsBelowDir(root, other_path):
646 """Check whether a path is below a root dir.
648 This works around the nasty byte-byte comparison of commonprefix.
651 if not (os.path.isabs(root) and os.path.isabs(other_path)):
652 raise ValueError("Provided paths '%s' and '%s' are not absolute" %
655 norm_other = os.path.normpath(other_path)
657 if norm_other == os.sep:
658 # The root directory can never be below another path
661 norm_root = os.path.normpath(root)
663 if norm_root == os.sep:
664 # This is the root directory, no need to add another slash
665 prepared_root = norm_root
667 prepared_root = "%s%s" % (norm_root, os.sep)
669 return os.path.commonprefix([prepared_root, norm_other]) == prepared_root
673 """Safe-join a list of path components.
676 - the first argument must be an absolute path
677 - no component in the path must have backtracking (e.g. /../),
678 since we check for normalization at the end
680 @param args: the path components to be joined
681 @raise ValueError: for invalid paths
684 # ensure we're having at least one path passed in
686 # ensure the first component is an absolute and normalized path name
688 if not IsNormAbsPath(root):
689 raise ValueError("Invalid parameter to PathJoin: '%s'" % str(args[0]))
690 result = os.path.join(*args)
691 # ensure that the whole path is normalized
692 if not IsNormAbsPath(result):
693 raise ValueError("Invalid parameters to PathJoin: '%s'" % str(args))
694 # check that we're still under the original prefix
695 if not IsBelowDir(root, result):
696 raise ValueError("Error: path joining resulted in different prefix"
697 " (%s != %s)" % (result, root))
701 def TailFile(fname, lines=20):
702 """Return the last lines from a file.
704 @note: this function will only read and parse the last 4KB of
705 the file; if the lines are very long, it could be that less
706 than the requested number of lines are returned
708 @param fname: the file name
710 @param lines: the (maximum) number of lines to return
713 fd = open(fname, "r")
717 pos = max(0, pos - 4096)
723 rows = raw_data.splitlines()
727 def BytesToMebibyte(value):
728 """Converts bytes to mebibytes.
731 @param value: Value in bytes
733 @return: Value in mebibytes
736 return int(round(value / (1024.0 * 1024.0), 0))
739 def CalculateDirectorySize(path):
740 """Calculates the size of a directory recursively.
743 @param path: Path to directory
745 @return: Size in mebibytes
750 for (curpath, _, files) in os.walk(path):
751 for filename in files:
752 st = os.lstat(PathJoin(curpath, filename))
755 return BytesToMebibyte(size)
758 def GetFilesystemStats(path):
759 """Returns the total and free space on a filesystem.
762 @param path: Path on filesystem to be examined
764 @return: tuple of (Total space, Free space) in mebibytes
767 st = os.statvfs(path)
769 fsize = BytesToMebibyte(st.f_bavail * st.f_frsize)
770 tsize = BytesToMebibyte(st.f_blocks * st.f_frsize)
771 return (tsize, fsize)
774 def ReadPidFile(pidfile):
775 """Read a pid from a file.
777 @type pidfile: string
778 @param pidfile: path to the file containing the pid
780 @return: The process id, if the file exists and contains a valid PID,
785 raw_data = ReadOneLineFile(pidfile)
786 except EnvironmentError, err:
787 if err.errno != errno.ENOENT:
788 logging.exception("Can't read pid file")
791 return _ParsePidFileContents(raw_data)
794 def _ParsePidFileContents(data):
795 """Tries to extract a process ID from a PID file's content.
799 @return: Zero if nothing could be read, PID otherwise
804 except (TypeError, ValueError):
805 logging.info("Can't parse pid file contents", exc_info=True)
811 def ReadLockedPidFile(path):
812 """Reads a locked PID file.
814 This can be used together with L{utils.process.StartDaemon}.
817 @param path: Path to PID file
818 @return: PID as integer or, if file was unlocked or couldn't be opened, None
822 fd = os.open(path, os.O_RDONLY)
823 except EnvironmentError, err:
824 if err.errno == errno.ENOENT:
825 # PID file doesn't exist
831 # Try to acquire lock
832 filelock.LockFile(fd)
833 except errors.LockError:
834 # Couldn't lock, daemon is running
835 return int(os.read(fd, 100))
842 def _SplitSshKey(key):
843 """Splits a line for SSH's C{authorized_keys} file.
845 If the line has no options (e.g. no C{command="..."}), only the significant
846 parts, the key type and its hash, are used. Otherwise the whole line is used
847 (split at whitespace).
856 if parts and parts[0] in constants.SSHAK_ALL:
857 # If the key has no options in front of it, we only want the significant
859 return (False, parts[:2])
861 # Can't properly split the line, so use everything
865 def AddAuthorizedKey(file_obj, key):
866 """Adds an SSH public key to an authorized_keys file.
868 @type file_obj: str or file handle
869 @param file_obj: path to authorized_keys file
871 @param key: string containing key
874 key_fields = _SplitSshKey(key)
876 if isinstance(file_obj, basestring):
877 f = open(file_obj, "a+")
884 # Ignore whitespace changes
885 if _SplitSshKey(line) == key_fields:
887 nl = line.endswith("\n")
891 f.write(key.rstrip("\r\n"))
898 def RemoveAuthorizedKey(file_name, key):
899 """Removes an SSH public key from an authorized_keys file.
902 @param file_name: path to authorized_keys file
904 @param key: string containing key
907 key_fields = _SplitSshKey(key)
909 fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
911 out = os.fdopen(fd, "w")
913 f = open(file_name, "r")
916 # Ignore whitespace changes while comparing lines
917 if _SplitSshKey(line) != key_fields:
921 os.rename(tmpname, file_name)
931 def DaemonPidFileName(name):
932 """Compute a ganeti pid file absolute path
935 @param name: the daemon name
937 @return: the full path to the pidfile corresponding to the given
941 return PathJoin(pathutils.RUN_DIR, "%s.pid" % name)
944 def WritePidFile(pidfile):
945 """Write the current process pidfile.
947 @type pidfile: string
948 @param pidfile: the path to the file to be written
949 @raise errors.LockError: if the pid file already exists and
950 points to a live process
952 @return: the file descriptor of the lock file; do not close this unless
953 you want to unlock the pid file
956 # We don't rename nor truncate the file to not drop locks under
958 fd_pidfile = os.open(pidfile, os.O_RDWR | os.O_CREAT, 0600)
960 # Lock the PID file (and fail if not possible to do so). Any code
961 # wanting to send a signal to the daemon should try to lock the PID
962 # file before reading it. If acquiring the lock succeeds, the daemon is
963 # no longer running and the signal should not be sent.
965 filelock.LockFile(fd_pidfile)
966 except errors.LockError:
967 msg = ["PID file '%s' is already locked by another process" % pidfile]
968 # Try to read PID file
969 pid = _ParsePidFileContents(os.read(fd_pidfile, 100))
971 msg.append(", PID read from file is %s" % pid)
972 raise errors.PidFileLockError("".join(msg))
974 os.write(fd_pidfile, "%d\n" % os.getpid())
979 def ReadWatcherPauseFile(filename, now=None, remove_after=3600):
980 """Reads the watcher pause file.
982 @type filename: string
983 @param filename: Path to watcher pause file
984 @type now: None, float or int
985 @param now: Current time as Unix timestamp
986 @type remove_after: int
987 @param remove_after: Remove watcher pause file after specified amount of
988 seconds past the pause end time
995 value = ReadFile(filename)
997 if err.errno != errno.ENOENT:
1001 if value is not None:
1005 logging.warning(("Watcher pause file (%s) contains invalid value,"
1006 " removing it"), filename)
1007 RemoveFile(filename)
1010 if value is not None:
1011 # Remove file if it's outdated
1012 if now > (value + remove_after):
1013 RemoveFile(filename)
1023 """Returns a random UUID.
1025 @note: This is a Linux-specific method as it uses the /proc
1030 return ReadFile(constants.RANDOM_UUID_FILE, size=128).rstrip("\n")
1033 class TemporaryFileManager(object):
1034 """Stores the list of files to be deleted and removes them on demand.
1044 def Add(self, filename):
1045 """Add file to list of files to be deleted.
1047 @type filename: string
1048 @param filename: path to filename to be added
1051 self._files.append(filename)
1053 def Remove(self, filename):
1054 """Remove file from list of files to be deleted.
1056 @type filename: string
1057 @param filename: path to filename to be deleted
1060 self._files.remove(filename)
1063 """Delete all files marked for deletion
1067 RemoveFile(self._files.pop())
1070 def IsUserInGroup(uid, gid):
1071 """Returns True if the user belongs to the group.
1074 @param uid: the user id
1076 @param gid: the group id
1080 user = pwd.getpwuid(uid)
1081 group = grp.getgrgid(gid)
1082 return user.pw_gid == gid or user.pw_name in group.gr_mem
1085 def CanRead(username, filename):
1086 """Returns True if the user can access (read) the file.
1088 @type username: string
1089 @param username: the name of the user
1090 @type filename: string
1091 @param filename: the name of the file
1095 filestats = os.stat(filename)
1096 user = pwd.getpwnam(username)
1098 user_readable = filestats.st_mode & stat.S_IRUSR != 0
1099 group_readable = filestats.st_mode & stat.S_IRGRP != 0
1100 return ((filestats.st_uid == uid and user_readable)
1101 or (filestats.st_uid != uid and
1102 IsUserInGroup(uid, filestats.st_gid) and group_readable))