[27560] in Source-Commits
pyhesiodfs commit: Add config file, attachtab, and locker.py support
daemon@ATHENA.MIT.EDU (Jonathan D Reed)
Tue Dec 31 12:27:31 2013
Date: Tue, 31 Dec 2013 12:27:24 -0500
From: Jonathan D Reed <jdreed@MIT.EDU>
Message-Id: <201312311727.rBVHROnP027110@drugstore.mit.edu>
To: source-commits@MIT.EDU
https://github.com/mit-athena/pyhesiodfs/commit/f9ab421f9df7c6391ccc4cbe2e0b03846d6ac455
commit f9ab421f9df7c6391ccc4cbe2e0b03846d6ac455
Author: Jonathan Reed <jdreed@mit.edu>
Date: Thu Nov 14 15:21:17 2013 -0500
Add config file, attachtab, and locker.py support
A large rewrite to add support for a configuration file, an "attachtab"
to present mount information to userspace tools, and using locker.py for
locker lookup and handling.
Changes as part of this commit:
- IOErrors are no longer raised during lookup, instead None is returned
and errors are syslogged.
- Whether or not to syslog for ERR lockers, unmountable lockers, or
successful mounts is configurable in the file.
- We override Fuse.parse() and store the mountpoint for later access
- Mount data is now stored in an attachtab object which stores both
user-requested symlinks and Locker objects
- Use new-style format strings in creating the readme
N.B. The attachtab does not expose other users' mounts, as the old
liblocker did. The decision to not expose other users' mounts in
pyhesiodfs was deliberate, and not simply a side-effect of
implementation.
pyHesiodFS.py | 228 ++++++++++++++++++++++++++++++++++++++++++++++++---------
1 files changed, 194 insertions(+), 34 deletions(-)
diff --git a/pyHesiodFS.py b/pyHesiodFS.py
index 6d51f77..a5c3029 100644
--- a/pyHesiodFS.py
+++ b/pyHesiodFS.py
@@ -1,6 +1,7 @@
#!/usr/bin/python2
# pyHesiodFS:
+# Copyright (c) 2013 Massachusetts Institute of Technology
# Copyright (C) 2007 Quentin Smith <quentin@mit.edu>
# "Hello World" pyFUSE example:
# Copyright (C) 2006 Andrew Straw <strawman@astraw.com>
@@ -17,9 +18,85 @@ import os, stat, errno, time
from syslog import *
import fuse
from fuse import Fuse
+from ConfigParser import RawConfigParser
from collections import defaultdict
-import hesiod
+import locker
+
+ATTACHTAB_PATH='.attachtab'
+
+class PyHesiodFSConfigParser(RawConfigParser):
+ """
+ A subclass of RawConfigParser that provides a single place to
+ store defaults, and ensures a section exists, along with
+ per-platform default values for the config file. Also override
+ getboolean to provide a method that deals with invalid values.
+ """
+ CONFIG_FILES = { 'darwin': '/Library/Preferences/PyHesiodFS.ini',
+ '_DEFAULT': '/etc/pyhesiodfs/config.ini',
+ }
+
+ CONFIG_DEFAULTS = { 'show_readme': 'false',
+ 'readme_filename': 'README.txt',
+ 'readme_contents': """
+This is the pyhesiodfs FUSE autmounter.
+{blank}
+To access a Hesiod filsys, just access {mountpoint}/name.
+{blank}
+If you're using the Finder, try pressing Cmd+Shift+G and then
+entering {mountpoint}/name""",
+ 'syslog_unavail': 'true',
+ 'syslog_unknown': 'true',
+ 'syslog_success': 'false',
+ }
+
+ def __init__(self):
+ RawConfigParser.__init__(self, defaults=self.CONFIG_DEFAULTS)
+ self.add_section('PyHesiodFS')
+ if sys.platform in self.CONFIG_FILES:
+ self.read(self.CONFIG_FILES[sys.platform])
+ else:
+ self.read(self.CONFIG_FILES['_DEFAULT'])
+
+ def getboolean(self, section, option):
+ try:
+ return RawConfigParser.getboolean(self, section, option)
+ except ValueError:
+ rv = RawConfigParser.getboolean(self, 'DEFAULT', option)
+ syslog(LOG_WARNING,
+ "Invalid boolean value for %s in config file; assuming %s" % (option, rv))
+ return rv
+
+class attachtab():
+ """
+ A dict-like class that stores both normal symlinks and locker
+ mounts, and also "serializes" them into the attachtab file
+ """
+ def __init__(self, fusefs):
+ self._mounts = defaultdict(dict)
+ self.fusefs = fusefs
+
+ def __getitem__(self, key):
+ value = self._mounts[self.fusefs._uid()][key]
+ return value.path
+
+ def __setitem__(self, key, value):
+ self._mounts[self.fusefs._uid()][key] = value
+
+ def __contains__(self, item):
+ return self._mounts[self.fusefs._uid()].__contains__(item)
+
+ def __delitem__(self, key):
+ del self._mounts[self.fusefs._uid()][key]
+
+ def mounts(self):
+ return self._mounts[self.fusefs._uid()].keys()
+
+ def __str__(self):
+ rv = []
+ for k, v in self._mounts[self.fusefs._uid()].items():
+ rv.append(v._serialize())
+ return "\n".join(rv) + "\n"
class negcache(dict):
"""
@@ -65,6 +142,32 @@ class MyStat(fuse.Stat):
self.st_mtime = 0
self.st_ctime = 0
+class FakeFiles(dict):
+ """A dict-style object that holds pathnames which behave as fake
+ read-only files, and their contents. Constraints on the keys and
+ values are enforced by raising ValueError or TypeError.
+ """
+ def __init__(self, path='/'):
+ super(FakeFiles, self).__init__()
+ self.path = path
+
+ def __setitem__(self, k, v):
+ if type(k) is not str:
+ raise TypeError('Filenames must be strings')
+ if type(v) is not str and not callable(v):
+ raise TypeError('File contents must be strings or callable')
+ f = k.strip()
+ if f in ['.', '..', ''] or '/' in f:
+ raise ValueError("Invalid filename: '%s'" % (k,))
+ super(FakeFiles, self).__setitem__(self.path + f,v)
+
+ def filenames(self):
+ return [x[len(self.path):] for x in self]
+
+ def __getitem__(self, k):
+ v = super(FakeFiles, self).__getitem__(k)
+ return v() if callable(v) else v
+
class PyHesiodFS(Fuse):
def __init__(self, *args, **kwargs):
@@ -82,12 +185,44 @@ class PyHesiodFS(Fuse):
self.fuse_args.add("noapplexattr", True)
self.fuse_args.add("volname", "MIT")
self.fuse_args.add("fsname", "pyHesiodFS")
- self.mounts = defaultdict(dict)
+ self.attachtab = attachtab(self)
+ self.files = FakeFiles()
+
+ self.syslog_unavail = True
+ self.syslog_unknown = True
+ self.syslog_success = False
+
# Cache deletions for half a second - should give `ln -nsf`
# enough time to make a new symlink
self.negcache = defaultdict(negcache)
+ def parse(self, *args, **kwargs):
+ Fuse.parse(self, *args, **kwargs)
+ self.mountpoint = self.fuse_args.mountpoint
+ # Ensure that we know where we're mounted at this point
+ assert self.mountpoint is not None
+
+ def _initializeConfig(self, config):
+ self.syslog_unavail = config.getboolean('PyHesiodFS', 'syslog_unavail')
+ self.syslog_unknown = config.getboolean('PyHesiodFS', 'syslog_unknown')
+ self.syslog_success = config.getboolean('PyHesiodFS', 'syslog_success')
+ self.show_readme = config.getboolean('PyHesiodFS', 'show_readme')
+
+ if self.show_readme:
+ try:
+ contents = config.get('PyHesiodFS', 'readme_contents') + "\n"
+ self.files[config.get('PyHesiodFS', 'readme_filename')] = \
+ contents.format(mountpoint=self.mountpoint, blank='')
+ except ValueError as e:
+ syslog(LOG_WARNING,
+ "config file: bad value for 'readme_filename'")
+ except KeyError as e:
+ syslog(LOG_WARNING,
+ "config file: bad substitution key (%s) in 'readme_contents'" % (e.message,))
+
+ self.files[ATTACHTAB_PATH] = self.attachtab.__str__
+
def _uid(self):
return fuse.FuseGetContext()['uid']
@@ -103,6 +238,10 @@ class PyHesiodFS(Fuse):
st.st_mode = stat.S_IFDIR | 0755
st.st_gid = self._gid()
st.st_nlink = 2
+ elif path in self.files:
+ st.st_mode = stat.S_IFREG | 0444
+ st.st_nlink = 1
+ st.st_size = len(self.files[path])
elif '/' not in path[1:]:
if path[1:] not in self.negcache[self._uid()] and self.findLocker(path[1:]):
st.st_mode = stat.S_IFLNK | 0777
@@ -115,41 +254,37 @@ class PyHesiodFS(Fuse):
return -errno.ENOENT
return st
- def getCachedLockers(self):
- return self.mounts[self._uid()].keys()
-
def findLocker(self, name):
"""Lookup a locker in hesiod and return its path"""
- if name in self.mounts[self._uid()]:
- return self.mounts[self._uid()][name]
+ if name in self.attachtab:
+ return self.attachtab[name]
else:
try:
- filsys = hesiod.FilsysLookup(name)
- except IOError, e:
- if e.errno in (errno.ENOENT, errno.EMSGSIZE):
- raise IOError(errno.ENOENT, os.strerror(errno.ENOENT))
- else:
- raise IOError(errno.EIO, os.strerror(errno.EIO))
- # FIXME check if the first locker is valid
- if len(filsys.filsys) >= 1:
- pointers = filsys.filsys
- pointer = pointers[0]
- if pointer['type'] == 'AFS' or pointer['type'] == 'LOC':
- self.mounts[self._uid()][name] = pointer['location']
- syslog(LOG_INFO, "Mounting "+name+" on "+pointer['location'])
- return pointer['location']
- elif pointer['type'] == 'ERR':
- syslog(LOG_NOTICE, "ERR for locker %s: %s" % (name, pointer['message'], ))
- return None
- else:
- syslog(LOG_NOTICE, "Unknown locker type "+pointer['type']+" for locker "+name+" ("+repr(pointer)+" )")
- return None
- else:
- syslog(LOG_WARNING, "Couldn't find filsys for "+name)
+ lockers = locker.lookup(name)
+ except locker.LockerNotFoundError as e:
+ if self.syslog_unknown:
+ syslog(LOG_NOTICE, str(e))
return None
+ except locker.LockerUnavailableError as e:
+ if self.syslog_unavail:
+ syslog(LOG_NOTICE, str(e))
+ return None
+ except locker.LockerError as e:
+ syslog(LOG_WARNING, str(e))
+ return None
+ # TODO: Check if the first locker is valid
+ # See Debathena Trac #583
+ for l in lockers:
+ if l.automountable():
+ self.attachtab[name] = l
+ if self.syslog_success:
+ syslog(LOG_INFO, "Mounting "+name+" on "+l.path)
+ return l.path
+ syslog(LOG_WARNING, "Lookup succeeded for %s but no lockers could be attached." % (name))
+ return None
def getdir(self, path):
- return [(i, 0) for i in (['.', '..'] + self.getCachedLockers())]
+ return [(i, 0) for i in (['.', '..'] + self.files.filenames() + self.attachtab.mounts())]
def readdir(self, path, offset):
for (r, zero) in self.getdir(path):
@@ -158,31 +293,56 @@ class PyHesiodFS(Fuse):
def readlink(self, path):
return self.findLocker(path[1:])
+ def open(self, path, flags):
+ if path not in self.files:
+ return -errno.ENOENT
+ accmode = os.O_RDONLY | os.O_WRONLY | os.O_RDWR
+ if (flags & accmode) != os.O_RDONLY:
+ return -errno.EACCES
+
+ def read(self, path, size, offset):
+ if path not in self.files:
+ return -errno.ENOENT
+ contents = self.files[path]
+ slen = len(contents)
+ if offset < slen:
+ if offset + size > slen:
+ size = slen - offset
+ buf = contents[offset:offset+size]
+ else:
+ buf = ''
+ return buf
+
def symlink(self, src, path):
- if path == '/':
+ if path == '/' or path in self.files:
return -errno.EPERM
elif '/' not in path[1:]:
- self.mounts[self._uid()][path[1:]] = src
+ self.attachtab[path[1:]] = locker.fromSymlink(src,
+ path[1:],
+ self.mountpoint)
self.negcache[self._uid()].remove(path[1:])
else:
return -errno.EPERM
def unlink(self, path):
- if path == '/':
+ if path == '/' or path in self.files:
return -errno.EPERM
elif '/' not in path[1:]:
- del self.mounts[self._uid()][path[1:]]
+ del self.attachtab[path[1:]]
self.negcache[self._uid()].add(path[1:])
else:
return -errno.EPERM
def main():
+ config = PyHesiodFSConfigParser()
+
usage = Fuse.fusage
server = PyHesiodFS(version="%prog " + fuse.__version__,
usage=usage,
dash_s_do='setsingle')
server.parse(errex=1)
+ server._initializeConfig(config)
try:
server.main()
except fuse.FuseError as fe: