[27548] in Source-Commits

home help back first fref pref prev next nref lref last post

locker-support commit: Initial checkin of implementation

daemon@ATHENA.MIT.EDU (Jonathan D Reed)
Wed Dec 18 12:15:00 2013

Date: Wed, 18 Dec 2013 12:14:53 -0500
From: Jonathan D Reed <jdreed@MIT.EDU>
Message-Id: <201312181714.rBIHEraK001888@drugstore.mit.edu>
To: source-commits@MIT.EDU

https://github.com/mit-athena/locker-support/commit/dd41937641b4ec15dfa6802f79ec800c04ec11b3
commit dd41937641b4ec15dfa6802f79ec800c04ec11b3
Author: Jonathan Reed <jdreed@mit.edu>
Date:   Thu Oct 10 15:04:38 2013 -0400

    Initial checkin of implementation
    
    Add initial checkin of liblocker, attach, and athdir
    replacements.

 NOTES     |   41 ++++++
 athdir    |   85 +++++++++++
 athdir.py |  264 ++++++++++++++++++++++++++++++++++
 attach    |  273 +++++++++++++++++++++++++++++++++++
 detach    |  129 +++++++++++++++++
 fsid      |  116 +++++++++++++++
 locker.py |  475 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 quota     |  140 ++++++++++++++++++
 8 files changed, 1523 insertions(+), 0 deletions(-)

diff --git a/NOTES b/NOTES
new file mode 100644
index 0000000..a00e8bf
--- /dev/null
+++ b/NOTES
@@ -0,0 +1,41 @@
+Development notes for the current version.  This file is referenced by
+footnotes in the source code in various locations.
+
+1. The old suite of attach commands (attach, detach, fsid, nfsid, zinit)
+   all took "-P" as their first argument to allow them to masquerade as
+   another member of the suite.  Spaces between -P and the value were
+   not allowed.  For bonus points, add(1) also took a separate -P
+   argument ("-P athena_path"), but that syntax has been obsolete for
+   years.  The current version replicates the check used in suite.c to
+   figure out if attach(1) is being called as add(1).  Masquerading as
+   anything else (e.g. attach -Pfsid) is no longer supported, and was
+   never a good idea to begin with.
+
+2. It is possible to end up with positional arguments from both parsers,
+   albeit not with the default shell aliases.  But the old syntax
+   supported it, so we will too, even though it's dumb.
+   (e.g. attach -Padd -b barnowl -a -h consult)
+
+3. The old syntax allowed options (-e) to directly modify the positional
+   argument that followed them, rather than changing the behavior of the
+   command.  (e.g. attach -h consult -e small-gods:/slackware/current,
+   where "-h" only applies to consult).  That is annoying to implement
+   with OptionParser, and not worth doing.
+
+4. The old add(1) looked at the first positional argument.  If it was a
+   path, all subsequent arguments were required to be paths, and
+   specifying a locker was an error (e.g. add /var/local consult).  If
+   the first positional argument was a locker name, all subsequent
+   arguments were interpreted as locker names.  For example, "add
+   consult /usr/games" would add the consult locker, and attempt to
+   lookup the "/usr/games" locker in Hesiod.  This was dumb, and now we
+   require that the user not mix paths and locker names.  Paths in the
+   cwd must be prefixed with "./"
+
+5. Despite the fact that add(1) operates on $PATH, we always printed the
+   path using $path syntax (space-separated) in tcsh mode, for legacy
+   reasons.
+
+6. The old quota(1) command bailed if any locker was specified by name
+   that was not attached.  It did not even attempt to process valid
+   lockers.  So we'll emulate that behavior.
diff --git a/athdir b/athdir
new file mode 100755
index 0000000..5aa1fea
--- /dev/null
+++ b/athdir
@@ -0,0 +1,85 @@
+#!/usr/bin/python
+
+import athdir
+import sys
+import logging
+from optparse import OptionParser
+
+def path_callback(option, opt_str, value, parser):
+    assert value is None
+    value = []
+    for arg in parser.rargs:
+        if arg[:2] == "--" and len(arg) > 2:
+            break
+        if arg[:1] == "-" and len(arg) > 1:
+            break
+        value.append(arg)
+
+    del parser.rargs[:len(value)]
+    setattr(parser.values, option.dest, value)
+
+usage = """%prog path [type]
+   or: athdir [-t type] [-p path ...] [-e] [-c] [-l] [-d | -i]
+       [-r recsep] [-f format] [-s sysname] [-m machtype]"""
+parser = OptionParser(usage=usage)
+parser.set_defaults(suppressEditorials=False, suppressSearch=False,
+                    listAll=False, forceIndependent=False,
+                    forceDependent=False, recsep="\n",
+                    lockerpath=['%p'], type='%t',
+                    format=None, sysname=None,
+                    machtype=None, debug=False)
+parser.add_option("-t", dest="type", action="store",
+                  help="directory type (e.g. 'bin', 'lib', ...)")
+parser.add_option("-p", dest="lockerpath", action="callback",
+                  callback=path_callback, help="One or more paths")
+parser.add_option("-e", dest="suppressEditorials", action="store_true",
+                  help="Supress editorializing about legacy paths")
+parser.add_option("-c", dest="suppressSearch", action="store_true",
+                  help="Choose the first acceptable path")
+parser.add_option("-l", dest="listAll", action="store_true",
+                  help="List all search paths")
+parser.add_option("-i", dest="forceIndependent", action="store_true",
+                  help="Force athdir to choose a machine-independent path")
+parser.add_option("-d", dest="forceDependent", action="store_true",
+                  help="Force athdir to choose a machine-dependent path")
+parser.add_option("-r", dest="recsep", action="store",
+                  help="Override the record separator (newline)")
+parser.add_option("-f", dest="format", action="store",
+                  help="Supply a custom path format string")
+parser.add_option("-s", dest="sysname", help="Override ATHENA_SYS value")
+parser.add_option("-m", dest="machtype", help="Override machtype value")
+parser.add_option("--debug", dest="debug", action="store_true",
+                  help="Verbose debugging")
+
+
+if len(sys.argv) < 2:
+    sys.exit(parser.get_usage())
+
+if len(sys.argv) == 3 and (True not in [ x.startswith('-') for x in sys.argv]):
+    a = athdir.Athdir(sys.argv[1], sys.argv[2])
+    print ''.join(a.get_paths())
+    sys.exit(0)
+
+(options, args) = parser.parse_args()
+if options.debug:
+    logging.basicConfig(level=logging.DEBUG)
+
+if (len(args) > 0):
+    sys.exit(parser.get_usage())
+
+if options.forceDependent and options.forceIndependent:
+    parser.error("-d and -i are mutually exclusive")
+
+if not options.suppressSearch and (options.forceDependent or
+                                   options.forceIndependent):
+    parser.error("-d and -i are meaningless without -c")
+
+paths = []
+for p in options.lockerpath:
+    a = athdir.Athdir(p, options.type, options.format,
+                      options.sysname, options.machtype)
+    paths += a.get_paths(options.suppressEditorials, options.suppressSearch,
+                         options.forceDependent, options.forceIndependent,
+                         options.listAll)
+print options.recsep.join(paths)
+sys.exit(0)
diff --git a/athdir.py b/athdir.py
new file mode 100644
index 0000000..50c9222
--- /dev/null
+++ b/athdir.py
@@ -0,0 +1,264 @@
+"""
+A Python re-implementation of Athena's libathdir
+"""
+import os
+import subprocess
+import sys
+import logging
+
+logger = logging.getLogger('athdir')
+
+def _machtype(arg=None):
+    """
+    Convenience function to run _machtype to get the canonical
+    values of -C and -S in the event the environment doesn't
+    have them.  Returns None or the output.  Per Debian policy,
+    it will first attempt to run machtype in PATH, then explicitly.
+    """
+    rv = None
+    for machtype in ['machtype', '/bin/machtype']:
+        cmd = [machtype]
+        if arg is not None:
+            cmd.append(arg)
+        try:
+            rv = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0].strip()
+            return rv
+        except OSError as e:
+            logger.info(e)
+    return rv
+
+class AthdirError(Exception):
+    pass
+
+class AthdirInternalError(AthdirError):
+    pass
+
+class Flavors():
+    """
+    An enum-esque thing representing the various "flavors" of
+    directories we can request.
+    """
+    ARCH = 1
+    SYS = 2
+    MACH = 3
+    PLAIN = 4
+
+    printableFlags = { 'A': ARCH,
+                       'M': MACH,
+                       'S': SYS,
+                       'P': PLAIN
+                       }
+
+
+class AthdirConvention():
+    """
+    The various types of "conventions" we have for paths, such
+    as arch-dependent, old-style sysnames and machnames, etc.
+    """
+    def __init__(self, template, custom=False):
+        self.template = template
+        self.atsys = '%s' in template
+        self.custom = custom
+        self.flavors = { Flavors.ARCH: 'arch' in template,
+                         Flavors.MACH: '%m' in template,
+                         Flavors.SYS: self.atsys and not 'arch' in template}
+        self.flavors[Flavors.PLAIN] = True not in self.flavors.values()
+
+    def dependencyMatch(self, dependent=False):
+        """
+        Return True if the convention's dependency matches the
+        dependency specified.  (e.g. .dependencyMatch(True) would
+        return True if the convention is arch dependent.)
+        """
+        return True if self.custom else (dependent != self.flavors[Flavors.PLAIN])
+
+    def acceptableFor(self, listOfAcceptable):
+        """
+        Return True if this convention is accepted for one or more of
+        the list of flavors provided.
+        """
+        if self.custom:
+            return True
+        for a in listOfAcceptable:
+            if self.flavors[a]:
+                return True
+        return False
+
+    def __repr__(self):
+        flags = { 'A': Flavors.ARCH,
+                  'M': Flavors.MACH,
+                  'S': Flavors.SYS,
+                  'P': Flavors.PLAIN
+                  }
+        return "AthdirConvention: %s (%s%s%s)" % (self.template, ''.join([k for k,v in Flavors.printableFlags.items() if self.flavors[v]]), '@' if self.atsys else '', 'C' if self.custom else '')
+
+class Athdir():
+    _indepTypes = ("man", "include")
+    # We are NOT adding any new ones below
+    _sysFlavors = ("bin", "lib", "etc")
+    _machFlavors = ("bin", "lib", "etc")
+
+    _conventions = (AthdirConvention("%p/arch/%s/%t"),
+                    AthdirConvention("%p/%s/%t"),
+                    AthdirConvention("%p/%m%t"),
+                    AthdirConvention("%p/%t"))
+
+    def __init__(self, basePath='%p', dirType='%t', customTemplate=None,
+                 sysName=None, hostType=None):
+        self.path = basePath
+        self.dirType = dirType
+        self.compatlist = [sysName if sysName is not None else self.sysname()] + self.syscompatlist()
+        self.hostType = hostType if hostType is not None else self.hosttype()
+        # for unknown types, assume arch dependent
+        self.archDependent = not dirType in self._indepTypes
+        # We always try the arch flavors
+        self.flavorsAcceptable = [ Flavors.ARCH ]
+        if dirType in self._sysFlavors:
+            logger.debug("Adding 'SYS' flavor")
+            self.flavorsAcceptable.append(Flavors.SYS)
+        if dirType in self._machFlavors:
+            logger.debug("Adding 'MACH' flavor")
+            self.flavorsAcceptable.append(Flavors.MACH)
+        if ((Flavors.SYS not in self.flavorsAcceptable) and
+            (Flavors.MACH not in self.flavorsAcceptable)):
+            logger.debug("Adding 'PLAIN' flavor")
+            self.flavorsAcceptable.append(Flavors.PLAIN)
+        self.conventions = list(self._conventions)
+        if customTemplate is not None:
+            logger.debug("Adding custom template: %s", customTemplate)
+            self.conventions.insert(0, AthdirConvention(customTemplate,
+                                                        custom=True))
+
+    def __repr__(self):
+        return "Athdir: %s (%s, %s)" % (self.path, self.dirType,
+                                        ''.join([k for k,v in Flavors.printableFlags.items() if v in self.flavorsAcceptable]))
+
+    def __str__(self):
+        return "Athdir: path=%s, type=%s, flags=%s, host=%s\n        compat=%s" % (self.path, self.dirType, ''.join([k for k,v in Flavors.printableFlags.items() if v in self.flavorsAcceptable]), self.hostType, self.compatlist)
+
+    def _expand(self, template, sys):
+        """
+        Expand a template by filling in substitutions.
+        """
+        rv = template
+        replacements = { '%s': sys,
+                         '%m': self.hostType,
+                         '%p': self.path,
+                         '%t': self.dirType }
+        for k,v in replacements.items():
+            rv = rv.replace(k, v)
+        return rv
+
+    def get_paths(self, suppressEditorials=False, suppressSearch=False,
+                  forceDependent=False, forceIndependent=False,
+                  listAll=False):
+        """
+        Get a list of acceptable paths for an athdir specification.
+
+        Parameters:
+        - suppressEditorals: If True, consider all possible conventions,
+          not just the "correct" ones.
+        - suppressSearch: If True, return the first appropriate path,
+          even if it doesn't exist.
+        - forceDependent: Used with suppressSearch.  If True, force an
+          arch-dependent path, even for things that are usually
+          arch-independent.
+        - forceIndependent: Used with suppressSearch.  If True, force an
+          arch-independent path, even for things that are usually
+          arch-dependent.
+        - listAll: If True, list all possible paths, expanding as
+          appropriate.
+        """
+        if forceDependent and forceIndependent:
+            raise AthdirError("forceDependent and forceIndependent are mutually exclusive.")
+        if (forceDependent or forceIndependent) and not suppressSearch:
+            raise AthdirError("forceDependent and forceIndependent are meaningless without suppressSearch.")
+        rv = []
+        if forceDependent:
+            logger.debug("Forcing architecture-dependent")
+            self.archDependent = True
+        if forceIndependent:
+            logger.debug("Forcing architecture-independent")
+            self.archDependent = False
+        for c in self.conventions:
+            logger.debug("** Considering %s", c)
+            if (c.acceptableFor(self.flavorsAcceptable) or
+                suppressEditorials or
+                ((forceDependent or forceIndependent) and suppressSearch)):
+                if suppressSearch and not c.dependencyMatch(self.archDependent):
+                    logger.debug("discarding %s", c)
+                    continue
+                for compat in self.compatlist:
+                    logger.debug("Considering sysname %s", compat)
+                    path = self._expand(c.template, compat)
+                    logger.debug("Expanding to %s", path)
+                    if listAll or suppressSearch:
+                        rv.append(path)
+                        logger.debug("Storing %s", path)
+                        if suppressSearch:
+                            logger.debug("Returning...")
+                            return rv
+                    elif os.path.exists(path):
+                        logger.debug("Path %s exists, returning...", path)
+                        rv.append(path)
+                        return rv
+                    if not c.atsys:
+                        logger.debug("Skipping sysname iteration")
+                        break
+        return rv
+
+    @staticmethod
+    def sysname():
+        """
+        Attempt to determine the sysname
+        """
+        sysname = os.getenv("ATHENA_SYS")
+        if sysname is not None:
+            return sysname
+        logger.info("ATHENA_SYS is unset")
+        sysname = _machtype('-S')
+        if sysname is not None:
+            return sysname
+        else:
+            raise AthdirInternalError("Unable to determine sysname.")
+
+    @staticmethod
+    def syscompatlist():
+        """
+        Attempt to determine the sysname compat list
+        """
+        compat = os.getenv("ATHENA_SYS_COMPAT")
+        if compat is not None:
+            return compat.split(':')
+        logger.info("ATHENA_SYS_COMPAT is unset")
+        compat = _machtype('-C')
+        if compat is not None:
+            return compat.split(':')
+        else:
+            raise AthdirInternalError("Unable to determine sysname compatibility list.")
+
+    @staticmethod
+    def hosttype():
+        """
+        Attempt to determine the machname (host type)
+        """
+        hosttype = os.getenv("HOSTTYPE")
+        if hosttype is not None:
+            return hosttype
+        logger.info("HOSTTYPE is unset")
+        hosttype = _machtype()
+        if hosttype is not None:
+            return hosttype
+        else:
+            raise AthdirInternalError("Unable to determine host type.")
+
+    @classmethod
+    def is_native(cls, somePath, sysn=None):
+        """
+        Determine if somePath is the native (i.e. not using
+        compatibility) sysname for the specified sysname (or current
+        platform)
+        """
+        if sysn is None:
+            sysn = cls.sysname()
+        return sysn in somePath
diff --git a/attach b/attach
new file mode 100755
index 0000000..1e4b243
--- /dev/null
+++ b/attach
@@ -0,0 +1,273 @@
+#!/usr/bin/python
+
+import sys
+import subprocess
+import logging
+import os
+import re
+from optparse import OptionParser
+
+import athdir
+import locker
+
+logger = logging.getLogger('attach')
+# Paths to be shortened when printing
+path_shorten_re = re.compile(r'/mit/([^/]+).*bin')
+# A map of athdir directories to environment variables
+varmap = { 'bin': 'PATH',
+           'man': 'MANPATH',
+           'info': 'INFOPATH' }
+
+class OrderedSet(list):
+    """
+    For de-duping lists.
+    (We don't have collections.OrderedDict in 2.6)
+    """
+    def __init__(self, iterable):
+        super(OrderedSet, self).__init__()
+        for x in iterable:
+            if x not in self:
+                self.append(x)
+
+    def remove(self, x):
+        """
+        Remove an item if it exists, without throwing ValueError
+        """
+        if x in self:
+            super(OrderedSet, self).remove(x)
+
+class Environment(dict):
+    varmap = { 'bin': 'PATH',
+               'man': 'MANPATH',
+               'info': 'INFOPATH' }
+
+    def __init__(self):
+        super(Environment, self).__init__()
+        for val in self.varmap.values():
+            self[val] = OrderedSet(os.getenv(val, '').split(':'))
+
+    def addLocker(self, mountpoint, options):
+        for subdir in varmap:
+            adir = athdir.Athdir(mountpoint, subdir)
+            paths = adir.get_paths()
+            assert len(paths) < 2
+            if subdir == 'bin' and options.warn:
+                if len(paths) == 0:
+                    print >>sys.stderr, \
+                        "%s: warning: %s has no binary directory" % \
+                        (sys.argv[0], mountpoint)
+                else:
+                    if False in [adir.is_native(p) for p in paths]:
+                        print >>sys.stderr, \
+                            "%s: warning: using compatibility for %s" % \
+                            (sys.argv[0], mountpoint)
+            for p in paths:
+                self[varmap[subdir]].remove(p)
+                if options.front:
+                    self[varmap[subdir]].insert(0,p)
+                else:
+                    self[varmap[subdir]].append(p)
+
+    def removeLocker(self, mountpoint):
+        for subdir in varmap:
+            adir = athdir.Athdir(mountpoint, subdir)
+            for path in adir.get_paths(listAll=True):
+                self[varmap[subdir]].remove(path)
+
+    def toShell(self, bourne=False):
+        # This can result in a spurious trailing colon for INFOPATH.
+        # Do we care?
+        template = "setenv %s \"%s\""
+        if bourne:
+            template = "export %s=\"%s\""
+        rv = []
+        for val in self:
+            rv.append(template % (val, ':'.join(self[val])))
+        return "\n".join(rv)
+
+def shorten_path(p):
+    match = path_shorten_re.match(p)
+    if match is None:
+        return p
+    return "{add %s%s}" % (match.group(1),
+                           '' if athdir.Athdir.is_native(p) else '*')
+
+def attach_filesys(filesys, options):
+    logger.debug("Attaching %s", filesys)
+    if options.lookup:
+        try:
+            results = locker.resolve(filesys)
+            print "%s resolves to:" % (filesys)
+            for r in results:
+                print "%s %s%s" % \
+                    (r['type'], r['data'],
+                     ' ' + str(r['priority']) if r['priority'] > 0 else '')
+        except (locker.LockerNotFoundError, locker.LockerError) as e:
+            print >>sys.stderr, e
+    else:
+        # Multiple entries will only be returned for FSGROUPs
+        # which we want to try in order.  Once successful, we're done
+        filesystems = []
+        if options.explicit:
+            filesystems.append(locker.LOCLocker(options.mountpoint, " ".join([filesys, 'n', options.mountpoint])))
+        else:
+            try:
+                filesystems = locker.lookup(filesys)
+            except locker.LockerError as e:
+                print >>sys.stderr, e
+        for entry in filesystems:
+            logger.debug("Attempting to attach %s", entry)
+            if entry.authRequired or entry.authDesired:
+                try:
+                    subprocess.check_call(entry.getAuthCommandline())
+                except subprocess.CalledProcessError as e:
+                    print >>sys.stderr, "Error while authenticating:", e
+                    if entry.authRequired:
+                        return None
+            if options.mountpoint is not None:
+                entry.mountpoint = options.mountpoint
+            try:
+                entry.attach(force=options.force)
+            except locker.LockerError as e:
+                print >>sys.stderr, e
+                continue
+            if options.printpath:
+                print entry.mountpoint
+            elif options.verbose:
+                print "%s: %s attached to %s for filesystem %s" % \
+                      (sys.argv[0], entry.path, entry.mountpoint, entry.name)
+            return entry.mountpoint
+        return None
+
+
+# __main__
+
+addusage = """%prog [-f] [-r] [-q] [-w] [-a attachopts] locker [locker ...]
+       %prog [-f] [-r] pathname [pathname ...]
+       %prog [-p]"""
+
+def attachopts_callback(option, opt_str, value, parser):
+    # Consume all remaining values on the list
+    assert value is None
+    value = []
+    for arg in parser.rargs:
+        value.append(arg)
+    del parser.rargs[:len(value)]
+    setattr(parser.values, option.dest, value)
+
+addParser = OptionParser(usage=addusage, prog="add",
+                         add_help_option=False)
+addParser.add_option("-f", dest="front", action="store_true", default=False,
+                     help="Add to front of path")
+addParser.add_option("-r", dest="remove", action="store_true", default=False,
+                     help="Remove from path")
+addParser.add_option("-w", dest="warn", action="store_true", default=False,
+                     help="Warn when using compatibility")
+addParser.add_option("-p", dest="printpath", action="store_true", default=False,
+                     help="Print readable path")
+addParser.add_option("-b", dest="bourne", action="store_true", default=False,
+                     help="Output bourne shell syntax")
+addParser.add_option("-q", dest="deprecated", action="store_true",
+                     help="[deprecated]")
+addParser.add_option("-a", dest="attachopts", action="callback", default=[],
+                     callback=attachopts_callback,
+                     help="Pass options to attach")
+addParser.add_option("-?", "--help", action="help",
+                     help="show this help message and exit")
+
+attachusage = """%prog [-v | -q | -p] [-z | -h] locker [locker ...]
+       %prog [-l locker [locker ...]
+       %prog """
+attachParser = OptionParser(usage=attachusage, add_help_option=False)
+attachParser.set_defaults(zephyr=False, verbose=True, force=False,
+                          printpath=False, lookup=False, explicit=False, mountpoint=None)
+attachParser.add_option("-z", "--zephyr", dest="zephyr", action="store_true",
+                        help="Subscribe to zephyr notifications")
+attachParser.add_option("-h", "--nozephyr", dest="zephyr", action="store_false",
+                        help="Do not subscribe to zephyr notifications")
+attachParser.add_option("-v", "--verbose", dest="verbose", action="store_true",
+                        help="Display information when attaching")
+attachParser.add_option("-q", "--quiet", dest="verbose", action="store_false",
+                        help="Do not display information when attaching")
+attachParser.add_option("-p", "--printpath", dest="printpath",
+                        action="store_true",
+                        help="Print the mountpoint when attaching")
+attachParser.add_option("-l", "--lookup", dest="lookup", action="store_true",
+                        help="Lookup the locker and print the result")
+attachParser.add_option("-?", "--help", action="help",
+                        help="show this help message and exit")
+attachParser.add_option("-e", "--explicit", dest="explicit", action="store_true",
+                        help="Interpret the filesystem as an explicit path")
+attachParser.add_option("-m", "--mountpoint", dest="mountpoint", action="store",
+                        help="Override the mountpoint for the filesystem")
+attachParser.add_option("-f", "--force", dest="force", action="store_true",
+                        help="Force the attach, even if the mountpoint is in use")
+attachParser.add_option("--debug", dest="debug", action="store_true",
+                        default=False, help="Debugging mode")
+
+# See NOTES[1]
+argv=sys.argv[1:]
+if (len(argv) > 0) and (argv[0] == "-Padd"):
+    argv.pop(0)
+    (options, args) = addParser.parse_args(argv)
+    if (len(options.attachopts) > 0) and (options.remove or options.printpath):
+        addParser.error("-a cannot be used with -r or -p")
+    (atoptions, atargs) = attachParser.parse_args(options.attachopts)
+    # See NOTES[2] and NOTES[3]
+    lockers = []
+    paths = []
+    for x in args + atargs:
+        if '/' in x:
+            paths.append(x)
+        else:
+            lockers.append(x)
+    # See NOTES[4]
+    if len(paths) and len(lockers):
+        addParser.error("You can't mix pathnames and lockernames.")
+    if atoptions.explicit and atoptions.mountpoint is None:
+        addParser.error("Must pass -m to 'attach' when also passing -e")
+    if atoptions.explicit or atoptions.mountpoint is not None:
+        if len(lockers) > 1:
+            addParser.error("You cannot specify more than one locker when passing -m or -e to 'attach'")
+    # Attach operations done as part of add are always quiet
+    atoptions.verbose=False
+    env = Environment()
+    if options.printpath or len(lockers + paths) < 1:
+        # See NOTES[5]
+        pathsep=':' if options.bourne else ' '
+        print >>sys.stderr, pathsep.join([shorten_path(p) for p in env['PATH']])
+        sys.exit(0)
+    if options.remove:
+        for p in paths:
+            env['PATH'].remove(p)
+        at = None
+        for l in lockers:
+            if at is None:
+                at = locker.read_attachtab()
+            for filesys in lockers:
+                if filesys not in at:
+                    print >>sys.stderr, "%s: Not attached." % (filesys,)
+                continue
+            env.removeLocker(at.mountpoint)
+    else:
+        for filesys in lockers:
+            mountpoint = attach_filesys(filesys, atoptions)
+            if mountpoint is not None:
+                env.addLocker(mountpoint, options)
+        for p in paths:
+            env['PATH'].append(p)
+    print env.toShell(options.bourne)
+else:
+    (options, args) = attachParser.parse_args(argv)
+    if options.debug:
+        logging.basicConfig()
+        logger.setLevel(logging.DEBUG)
+    if len(args) < 1:
+        print locker.read_attachtab()._legacyFormat()
+    if options.explicit and not options.mountpoint:
+        attachParser.error("Must specify mountpoint (-m) when using -e")
+    if (options.explicit or options.mountpoint) and len(args) != 1:
+        attachParser.error("Must specify exactly one argument when using -e or -m")
+    for filesys in args:
+        attach_filesys(filesys, options)
+sys.exit(0)
diff --git a/detach b/detach
new file mode 100755
index 0000000..71571d2
--- /dev/null
+++ b/detach
@@ -0,0 +1,129 @@
+#!/usr/bin/python
+
+import sys, os
+import socket
+import logging
+from optparse import OptionParser
+import locker
+
+logger = logging.getLogger('detach')
+
+# We used to support passing different options to each filesystem
+# (e.g. detach -z consult -h sipb, which would unsubscribe you from
+# zephyr notifications for consult but not sipb), but that option
+# parsing code would suck.  Also, I don't care.
+#
+usage = """%prog [options] filesystem ...
+       %prog [options] mountpoint ...
+       %prog [options] -H host ...
+       %prog [options] -a"""
+
+def deprecated_callback(option, opt_str, value, parser):
+    """
+    An OptionParser callback for deprecated options
+    """
+    print >>sys.stderr, "WARNING: '%s' is obsolete and will be removed in future versions." % (opt_str)
+
+parser = OptionParser(usage=usage, add_help_option=False)
+parser.set_defaults(zephyr=False, unmap=True, verbose=True,
+                    all_filesys=False, explicit=False, fstype=[])
+parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
+                  help="Display information when detaching")
+parser.add_option("-q", "--quiet", dest="verbose", action="store_false",
+                  help="Do not display information when detaching")
+parser.add_option("-a", "--all", dest="all_filesys", action="store_true",
+                  help="Detach all attached filesystems")
+parser.add_option("-z", "--zephyr", dest="zephyr", action="store_true",
+                  help="Unsubscribe from zephyr notifications")
+parser.add_option("-h", "--nozephyr", dest="zephyr", action="store_false",
+                  help="Do not unsubscribe from zephyr notifications")
+parser.add_option("-y", "--unmap", dest="unmap", action="store_true",
+                  help="Attempt to remove authentication")
+parser.add_option("-n", "--nomap", dest="unmap", action="store_false",
+                  help="Do not attempt to remove authentication")
+parser.add_option("-t", "--type", dest="fstype", action="append",
+                  help="Limit -a operation to FSTYPE")
+# Do we want this in a FUSE world?
+parser.add_option("-H", "--host", dest="host", action="store",
+                  help="Detach all filesystems served from HOST")
+# Add the help option manually, because -h is already used for something else
+parser.add_option("-?", "--help", action="help",
+                  help="show this help message and exit")
+parser.add_option("--debug", dest="debug", action="store_true",
+                  default=False, help="Debugging mode")
+# Deprecated options
+# -C and -O are meaningless in a FUSE world
+# -C used to mean "clean": detach the filesys only if it's not wanted
+#    by anyone in /etc/passwd
+# -O used to mean "override": detach filesystems regardless of if they're
+#    wanted
+# -e used to mean "explicit": parse the argument as though it was
+#    host:directory (for NFS) or a path (for AFS), and convert it to a
+#    mount point (e.g. for NFS: /hostname/export).  This is
+#    meaningless now.  We may still support explicit attaches (to offload
+#    onto a /net automounter or something), but detaches can happen on
+#    the mountpoint.
+# -x was used to reverse -e's behavior.  We no longer support per-filesys
+#    options.
+parser.add_option("-C", "--clean", action="callback",
+                  callback=deprecated_callback, help="[obsolete]")
+parser.add_option("-O", "--override", action="callback",
+                  callback=deprecated_callback, help="[obsolete]")
+parser.add_option("-e", "--explicit", action="callback",
+                  callback=deprecated_callback, help="[obsolete]")
+parser.add_option("-x", "--noexplicit", action="callback",
+                  callback=deprecated_callback, help="[obsolete]")
+(options, args) = parser.parse_args()
+
+if options.debug:
+    logging.basicConfig()
+    logger.setLevel(logging.DEBUG)
+
+if options.all_filesys:
+    if len(args) != 0:
+        parser.error("-a does not take arguments.")
+
+if options.fstype and not options.all_filesys:
+    parser.error("-t is meaningless without -a")
+
+# Uppercase everything so it matches hesiod
+options.fstype = [x.upper() for x in options.fstype]
+
+attachtab = locker.read_attachtab()
+# TODO: Should we let them 'detach' their homedir?
+if options.host:
+    try:
+        hostent = socket.gethostbyname_ex(options.host)
+        hostset = set([hostent[0].lower()] + hostent[2])
+    except socket.herror:
+        sys.exit("Cannot lookup host '%s'" % (options.host))
+
+if options.all_filesys or options.host:
+    for l in attachtab:
+        if options.host:
+            try:
+                if len(hostset.intersection([x.lower() for x in attachtab[l].getFileServers()])) < 1:
+                    continue
+            except locker.LockerNotSupportedError:
+                continue
+        elif len(options.fstype) > 0 and attachtab[l]._type() not in options.fstype:
+            continue
+        try:
+            attachtab[l].detach()
+            if options.verbose:
+                print >>sys.stderr, "%s: %s detached" % (sys.argv[0], attachtab[l].name)
+        except locker.LockerError as e:
+            print >>sys.stderr, "%s: Unable to detach: %s" % (l, e)
+    sys.exit(0)
+
+for a in args:
+    if a in attachtab:
+        try:
+            attachtab[a].detach()
+            if options.verbose:
+                print >>sys.stderr, "%s: %s detached" % (sys.argv[0], attachtab[a].name)
+        except locker.LockerError as e:
+            print >>sys.stderr, "%s: Unable to detach: %s" % (l, e)
+    else:
+        print >>sys.stderr, "%s: Not attached." % (a,)
+sys.exit(0)
diff --git a/fsid b/fsid
new file mode 100755
index 0000000..a3fef74
--- /dev/null
+++ b/fsid
@@ -0,0 +1,116 @@
+#!/usr/bin/python
+
+import sys, os
+import logging
+from optparse import OptionParser
+import locker
+import subprocess
+
+logger = logging.getLogger('fsid')
+
+def do_map(l, options):
+    cmdline = l.getAuthCommandline()
+    if not options.map:
+        cmdline = l.getDeauthCommandline()
+    if cmdline is None:
+        print >>sys.stderr, "%s: %s: %smapping not supported" % (sys.argv[0],
+                                                                 l.name,
+                                                                 '' if options.map else 'un')
+    else:
+        try:
+            subprocess.check_call(cmdline)
+            if options.verbose:
+                print >>sys.stderr, "%s: %s mapped" % (sys.argv[0],
+                                                       l.name)
+        except subprocess.CalledProcessError as e:
+            print >>sys.stderr, "%s: Unable to map %s" % (sys.argv[0],
+                                                          l.name)
+        except locker.LockerError as e:
+            print >>sys.stderr, "%s: %s: %s" % (sys.argv[0],
+                                                l.name,
+                                                e)
+
+def deprecated_callback(option, opt_str, value, parser):
+    """
+    An OptionParser callback for deprecated options
+    """
+    print >>sys.stderr, "WARNING: '%s' is obsolete and will be removed in future versions." % (opt_str)
+
+usage = """%prog [options] [-f] filesystem ...
+       %prog [options] -c cell ...
+       %prog [options] -a"""
+
+parser = OptionParser(usage=usage, add_help_option=False)
+parser.set_defaults(verbose=True, map=True, all_filesys=False, cells=[],
+                    filesystems=[])
+parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
+                  help="Display information when mapping")
+parser.add_option("-q", "--quiet", dest="verbose", action="store_false",
+                  help="Do not display information")
+parser.add_option("-a", "--all", dest="all_filesys", action="store_true",
+                  help="Map to all attached filesystems")
+parser.add_option("-m", "--map", dest="map", action="store_true",
+                  help="Attempt to remove authentication")
+parser.add_option("-u", "--unmap", dest="map", action="store_false",
+                  help="Do not attempt to remove authentication")
+parser.add_option("-c", "--cell", dest="cells", action="append",
+                  help="Authenticate to the specified cell(s)")
+parser.add_option("-f", "--filsys", dest="filesystems", action="append",
+                  help="Map to the specified filesystems")
+# Add the help option manually, because -h is already used for something else
+parser.add_option("-?", "--help", action="help",
+                  help="show this help message and exit")
+parser.add_option("--debug", dest="debug", action="store_true",
+                  default=False, help="Debugging mode")
+# Deprecated options
+# -p purged host mappings for NFS
+# -r purged user mappins for NFS
+# -h operated on the specified host only (NFS)
+parser.add_option("-h", "--host", action="callback", type="string",
+                  callback=deprecated_callback, help="[obsolete]")
+parser.add_option("-p", "--purge", action="callback",
+                  callback=deprecated_callback, help="[obsolete]")
+parser.add_option("-r", "--purgeuser", action="callback",
+                  callback=deprecated_callback, help="[obsolete]")
+(options, args) = parser.parse_args()
+
+if options.debug:
+    logging.basicConfig()
+    logger.setLevel(logging.DEBUG)
+
+if len(args) != 0:
+    if options.all_filesys:
+        parser.error("-a does not take arguments.")
+    if len(options.filesystems):
+        parser.error("Cannot mix -f with other filesystems or hosts")
+
+if len(options.filesystems) and options.all_filesys:
+    parser.error("-a and -f are mutually exclusive")
+
+if len(options.cells):
+    stderr = None
+    try:
+        cmdline = ['aklog'] + sum((['-cell', x] for x in options.cells), [])
+        stderr = subprocess.Popen(cmdline,
+                                  stderr=subprocess.PIPE).communicate()[1]
+    except OSError as e:
+        sys.exit("Unable to run aklog: %s" % (e.strerror,))
+    sys.exit(0 if stderr is None else 1)
+
+try:
+    attachtab = locker.read_attachtab()
+except locker.LockerError as e:
+    sys.exit(e)
+
+if options.all_filesys:
+    for f in attachtab:
+        do_map(attachtab[f], options)
+    sys.exit(0)
+
+for f in options.filesystems + args:
+    if f in attachtab:
+        do_map(attachtab[f], options)
+    else:
+        print >>sys.stderr, "%s: %s not attached" % (sys.argv[0], f)
+
+sys.exit(0)
diff --git a/locker.py b/locker.py
new file mode 100644
index 0000000..6a95377
--- /dev/null
+++ b/locker.py
@@ -0,0 +1,475 @@
+"""
+A modern re-interpretation of Athena's liblocker, in Python.
+"""
+from __future__ import division
+import math
+import errno, re, os, pwd
+import logging
+import warnings
+
+import afs.fs
+import hesiod
+
+logger = logging.getLogger('locker')
+
+_classNameRE = re.compile(r'([A-Z]+)Locker')
+_mountpoint = '/mit'
+
+class LockerError(Exception):
+    """
+    Base class for Exceptions in this module.
+    """
+    def __init__(self, message):
+        self.message = message
+
+    def __str__(self):
+        return "Error: %s" % (self.message)
+
+    def __repr__(self):
+        return self.__str__()
+
+class NamedLockerError(LockerError):
+    """
+    Exceptions for which the locker name is known.
+    """
+    def __init__(self, name, message):
+        self.name = name
+        LockerError.__init__(self, message)
+
+    def __str__(self):
+        return "%s: %s" % (self.name, self.message)
+
+class LockerNotSupportedError(NamedLockerError):
+    """
+    The locker is not supported, or the operation is not supported
+    on lockers of this type.
+    """
+    def __init__(self, name, lockerType, operation=None):
+        msg = "'%s' lockers are not supported." % (lockerType)
+        if operation is not None:
+            msg = "'%s' operation not supported for '%s' lockers." % \
+                  (operation, lockerType)
+        NamedLockerError.__init__(self, name, msg)
+
+class LockerNotFoundError(NamedLockerError):
+    """
+    The locker does not exist.
+    """
+    def __init__(self, name):
+        NamedLockerError.__init__(self, name, "Locker unknown.")
+
+class LockerUnavailableError(NamedLockerError):
+    """
+    The locker exists, but is not available.
+    """
+    def __init__(self, name, message="Locker unavailable."):
+        NamedLockerError.__init__(self, name, message)
+
+class LockerQuota(dict):
+    """
+    Object for storing locker quota, in an extensible manner, that
+    also deals with KB vs KiB correctly, because pedantry.
+    """
+    def __init__(self, usage, maximum, units=1024):
+        """
+        Initialize a quota object, given a usage amount and a maximum.
+        Units determines what the typical metric prefixes mean.
+        """
+        if type(units) is not int or units not in [1000, 1024]:
+            raise TypeError("units must be 1000 or 1024")
+        dict.__init__(self)
+        self['usage'] = usage
+        self['max'] = maximum
+        self['units'] = units
+
+    def percentage(self):
+        # Historically, unlimited quotas in liblocker have been represented by
+        # a max value of 0 and a usage of 0%.  We could make this more friendly
+        # by returning -1 so as to differentiate between no limit and a usage
+        # so small so as to essentially use 0% of your quota after rounding.
+        if self['max'] == 0:
+            return 0
+        else:
+            return int((self['usage'] / self['max']) * 100)
+
+    def _sizeStr(num):
+        _suffixes = { 0: 'KB',
+                      1: 'MB',
+                      2: 'GB',
+                      3: 'TB',
+                      4: 'PB',
+                      }
+        i = float(num)
+        for power in sorted(_suffixes.keys()):
+            if i < self['units']:
+                break
+            i /= self['units']
+        return "%.1f %s" % (i, _suffixes[power])
+
+class Locker(object):
+    def __init__(self, name, data):
+        self.name = name
+        self._data = data
+        self.mountpoint = None
+        self.path = None
+        self.auth = None
+        self.authSupported = False
+        self.authRequired = False
+        self.authDesired = False
+        self.parseData()
+
+    def parseData(self):
+        pass
+
+    def getAuthCommandline(self):
+        """
+        Return the command line required to authenticate.
+        (suitable for passing to subprocess.Popen)
+        """
+        return None
+
+    def getDeauthCommandline(self):
+        """
+        Return the command line required to remove authentication.
+        (suitable for passing to subprocess.Popen)
+        """
+        return None
+
+    def getZephyrTriplets(self):
+        """
+        Return a list of 3-tuples representing Zephyr triplets
+        one should subscribe to when attaching this locker.
+        """
+        return []
+
+    def attach(self, **kwargs):
+        """
+        Attempt to attach the locker.
+        """
+        logger.debug("Attempting to attach %s...", self.name)
+        if self.mountpoint is None or self.path is None:
+            raise LockerNotSupportedError(self.name, self._type(), 'attach')
+        if not self.mountpoint.startswith(_mountpoint):
+            raise NamedLockerError(self.name, "mountpoint %s is not under %s" % (self.mountpoint, _mountpoint))
+        try:
+            os.symlink(self.path, self.mountpoint)
+        except OSError as e:
+            if e.errno == errno.EEXIST:
+                if 'force' in kwargs and kwargs['force']:
+                    os.remove(self.mountpoint)
+                    self.attach()
+                # Call it success if we're already attached
+                if not os.path.realpath(self.mountpoint) == \
+                       os.path.realpath(self.path):
+                    raise NamedLockerError(self.name,
+                                           "%s already attached on %s" % \
+                                           (os.path.realpath(self.mountpoint),
+                                            self.mountpoint))
+            else:
+                raise NamedLockerError(self.name,
+                                       e.strerror + " while attaching")
+
+    def detach(self):
+        """
+        Attempt to detach the locker.
+        """
+        logger.debug("Attempting to detach %s...", self.name)
+        if self.mountpoint is None:
+            raise LockerNotSupportedError(self.name, self._type(), 'detach')
+        if not self.mountpoint.startswith(_mountpoint):
+            raise NamedLockerError(self.name, "mountpoint %s is not under %s" % (self.mountpoint, _mountpoint))
+        try:
+            os.unlink(self.mountpoint)
+        except OSError as e:
+            raise NamedLockerError(self.name,
+                                   e.strerror + " while detaching")
+
+    def automountable(self):
+        """
+        Return True if the locker can be auto-mounted.
+        """
+        return self.mountpoint is not None
+
+    def getQuota(self):
+        """
+        Return a the quota as a dict-like object
+        """
+        raise LockerNotSupportedError(self.name, self._type(), "getQuota()")
+
+    def getFileServers(self):
+        """
+        Return a list of hostnames or IP addresses of file servers
+        which serve this locker.
+        """
+        raise LockerNotSupportedError(self.name, self._type(),
+                                      "getFileServers()")
+
+    def _type(self):
+        m = _classNameRE.match(self.__class__.__name__)
+        return m.group(1) if m is not None else None
+
+    def _serialize(self):
+        return "%s:%s:%s" % (self.name, self._type(),
+                             self._data)
+
+    def __str__(self):
+        return "%s -> %s" % (self.mountpoint, self.path)
+
+    def __repr__(self):
+        return "%s: %s (%s)" % (self.__class__.__name__,
+                                self.name,
+                                self.__str__())
+
+class LOCLocker(Locker):
+    """
+    A class representing LOC lockers, which are just
+    symlinks.  The mode bit is ignored.
+    e.g. LOC /u1/lockers/sipb w /mit/sipb
+    """
+    def __init__(self, name, data):
+        Locker.__init__(self, name, data)
+
+    def parseData(self):
+        parts = self._data.split(" ")
+        if len(parts) != 3:
+            raise NamedLockerError(self.name,
+                                   "Invalid LOC locker data (%s)" % \
+                                   (self._data,))
+        self.path = parts[0]
+        self.mountpoint = parts[2]
+        # The auth bit is unused, but present
+        self.auth = parts[1]
+
+class AFSLocker(Locker):
+    """
+    A class representing AFS lockers.
+    """
+    def __init__(self, name, data):
+        Locker.__init__(self, name, data)
+
+    def parseData(self):
+        parts = self._data.split(" ")
+        if len(parts) != 3:
+            raise NamedLockerError(self.name,
+                                   "Invalid AFS locker data (%s)" % \
+                                   (self._data,))
+        self.path = parts[0]
+        self.mountpoint = parts[2]
+        self.auth = parts[1]
+        self.authSupported = True
+        self.authRequired = self.auth == 'w'
+        self.authDesired = self.authRequired or (self.auth == 'r')
+
+    def getAuthCommandline(self):
+        return ['aklog', '-path', self.path]
+
+    def getZephyrTriplets(self):
+        rv = []
+        try:
+            cell = afs.fs.whichcell(self.path)
+            rv.append(('filsrv', cell+':root.cell', '*'))
+            rv.append(('filsrv', cell, '*'))
+            try:
+                volume = afs.fs.examine(self.path)[0].name
+                rv.append(('filsrv', cell+':'+volume, '*'))
+                # Because dirname is stupid if it ends in a trailing slash
+                parent_dir = os.path.dirname(os.path.normpath(self.path))
+                parent_vol = afs.fs.examine(parent_dir)[0].name
+                # TODO: This is a hack until afs.vos exists.  Once it
+                #       does, we should check if ParentId != Vid in
+                #       the VolumeStatus, and get the ParentId's name
+                if parent_vol.endswith('.readonly'):
+                    parent_vol = parent_vol[0:len(parent_vol)-9]
+                rv.append(('filsrv', cell+':'+parent_vol, '*'))
+            except:
+                pass
+        except:
+            pass
+        for f in self.getFileServers():
+            rv.append(('filsrv', f.lower(), '*'))
+        return rv
+
+    def getQuota(self):
+        try:
+            volstat = afs.fs.examine(self.path)[0]
+            return LockerQuota(volstat.BlocksInUse, volstat.MaxQuota)
+        except OSError as e:
+            raise LockerError("Error getting AFS quota: %s: %s" % (self.path,
+                              e.strerror))
+
+    def getFileServers(self):
+        try:
+            return afs.fs.whereis(self.path)
+        except:
+            return []
+
+class NFSLocker(Locker):
+    """
+    Stub support for NFS lockers.
+    """
+    def __init__(self, name, data):
+        Locker.__init__(self, name, data)
+
+    def parseData(self):
+        parts = self._data.split(" ")
+        if len(parts) != 4:
+            raise NamedLockerError(self.name,
+                                   "Invalid NFS locker data (%s)" % \
+                                   (self._data,))
+        self.path = parts[0]
+        self.mountpoint = parts[3]
+        self.auth = parts[2]
+        self.server = parts[1]
+
+    def getFileServers(self):
+        return [self.server]
+
+    def __str__(self):
+        return "%s -> %s:%s" % (self.mountpoint, self.server, self.path)
+
+
+class MULLocker(Locker):
+    """
+    Stub support for "MUL" lockers, which are pointers to
+    a list of lockers.
+    """
+
+    def __init__(self, name, data):
+        Locker.__init__(self, name, data)
+
+    def parseData(self):
+        self.sublockers = []
+        for l in self._data.split(" "):
+            self.sublockers.append(*lookup(l))
+
+    def __str__(self):
+        return ', '.join([x.__repr__() for x in self.sublockers])
+
+# A mapping of filesystem type as specified in the Hesiod record
+# to classes in this module.
+_lockerTypes = {
+    'AFS': AFSLocker,
+    'NFS': NFSLocker,
+    'MUL': MULLocker,
+    'LOC': LOCLocker,
+}
+
+def fromSymlink(src, dst, mountpoint):
+    path = os.path.join(mountpoint, dst)
+    return LOCLocker(dst, "%s n %s" % (src, path))
+
+def lookup(name):
+    """
+    Lookup a locker in Hesiod and return a list locker objects.  For
+    FSGROUPs, the list will be sorted based on the priority of each
+    record.
+
+    Raises: LockerUnavailableError, LockerNotFoundError, LockerError
+    """
+    filesystems = resolve(name)
+    lockers = []
+    for f in filesystems:
+        if f['type'] == 'ERR':
+            raise LockerUnavailableError(name, f['data'])
+        if f['type'] not in _lockerTypes:
+            raise LockerNotSupportedError(name, f['type'])
+        lockers.append(_lockerTypes[f['type']](name, f['data']))
+    return lockers
+
+def resolve(name):
+    """
+    Lookup a locker in Hesiod and return a list of dictionaries, with
+    keys 'priority', 'data', and 'type'.   If the lookup found an FSGROUP,
+    the list will be sorted based on key the key 'priority'.
+
+    Raises: LockerNotFoundError, LockerError
+    """
+    filesystems = []
+    # Avoid generating a confusing "message too long" error from
+    # Hesiod.
+    if name.startswith('.'):
+        raise LockerError("Invalid locker name: " + name)
+    try:
+        filesystems = hesiod.FilsysLookup(name, parseFilsysTypes=False).filsys
+    except IOError as e:
+        if e.errno == errno.ENOENT:
+            raise LockerNotFoundError(name)
+        else:
+            raise LockerError("Hesiod Error: %s while resolving %s" % \
+                              (e.strerror if e.strerror else e.message, name))
+    return filesystems
+
+def ellipsize(text, maxlen):
+    if len(text) <= maxlen:
+        return text
+    cutoff = (maxlen - 5) / 2
+    return text[:int(math.ceil(cutoff))] + '[...]' + text[-int(math.floor(cutoff)):]
+
+class attachtab(dict):
+    """
+    A magic dictionary with magic versions of __getitem__ and __contains__
+    which allows the caller to access lockers by name or mountpoint.
+    (This may or may not be a good idea.)
+    """
+    def __init__(self):
+        dict.__init__(self)
+
+    def __getitem__(self, key):
+        if '/' in key:
+            return dict.__getitem__(self, key)
+        else:
+            for k,v in self.items():
+                if v.name == key:
+                    return v
+            raise KeyError(key)
+
+    def __contains__(self, key):
+        if '/' in key:
+            return dict.__contains__(self, key)
+        else:
+            for k,v in self.items():
+                if v.name == key:
+                    return True
+            return False
+
+    def _legacyFormat(self):
+        fmt = "%-30s %-26s %-9s %s\n"
+        rv = fmt % ("filesystem", "mountpoint", "user", "mode")
+        rv += fmt % ("----------", "----------", "----", "----")
+        uid = os.getuid()
+        try:
+            username = pwd.getpwuid(uid).pw_name
+        except Exception as e:
+            username = "uid %d" % (uid,)
+        for k,v in self.items():
+            fs = v.name if v._type() != 'LOC' else v.path
+            mode = v.auth if v.auth is not None else 'n'
+            mode += ',nosuid'
+            rv += fmt % (ellipsize(fs, 30), k, username, mode)
+        return rv
+
+def read_attachtab(mountpoint=_mountpoint):
+    """
+    Read the attachtab and return a dict() of
+    mountpoint:Locker
+    """
+    rv = attachtab()
+    try:
+        with open(os.path.join(mountpoint, '.attachtab'), 'r') as f:
+            for line in f:
+                line = line.strip()
+                if len(line) == 0:
+                    continue
+                parts = line.strip().split(':', 2)
+                assert len(parts) == 3
+                assert parts[0] not in rv
+                assert parts[1] in _lockerTypes
+                locker_mtpt = os.path.join(mountpoint,parts[0])
+                rv[locker_mtpt] = _lockerTypes[parts[1]](parts[0],
+                                                         parts[2])
+                if locker_mtpt != rv[locker_mtpt].mountpoint:
+                    warnings.warn("Mountpoint mismatch for locker %s" % \
+                                      (parts[0]))
+    except IOError as e:
+        raise LockerError("Failed to read attachtab: %s" % (e,))
+    return rv
diff --git a/quota b/quota
new file mode 100755
index 0000000..9b16751
--- /dev/null
+++ b/quota
@@ -0,0 +1,140 @@
+#!/usr/bin/python
+
+import sys
+import locker
+import os
+import pwd
+import logging
+from optparse import OptionParser
+
+logger = logging.getLogger('quota')
+
+usage = "%prog [-v] [-a | -f filesystem [-f filesystem ...]]"
+
+def deprecated_callback(option, opt_str, value, parser):
+    """
+    An OptionParser callback for deprecated options
+    """
+    print >>sys.stderr, "WARNING: '%s' is obsolete and will be removed in future versions." % (opt_str)
+
+parser = OptionParser(usage=usage)
+parser.set_defaults(verbose=False, all_filesys=False, parsable=False,
+                    filesys=[])
+parser.add_option("-v", dest="verbose", action="store_true",
+                  help="Display quotas where we have write permission")
+parser.add_option("-a", dest="all_filesys", action="store_true",
+                  help="Display quotas for all attached lockers")
+parser.add_option("-f", dest="filesys", action="append",
+                  help="Operation only on this filesystem")
+parser.add_option("--parsable", dest="parsable", action="store_true",
+                  help="Output suitable for parsing")
+parser.add_option("--debug", dest="debug", action="store_true",
+                  default=False, help="Debugging mode")
+# Deprecated options
+parser.add_option("-u", action="callback",
+                  callback=deprecated_callback, help="[obsolete]")
+parser.add_option("-g", action="callback",
+                  callback=deprecated_callback, help="[obsolete]")
+
+(options, args) = parser.parse_args()
+
+if options.debug:
+    logging.basicConfig()
+    logger.setLevel(logging.DEBUG)
+
+if len(args) > 0:
+    parser.error("command no longer takes any arguments (i.e. no usernames).")
+
+if options.all_filesys:
+    if len(options.filesys) > 0:
+        parser.error("-a and -f are mutually exclusive")
+    if options.parsable:
+        parser.error("-a cannot be used with --parsable")
+
+if options.parsable and not options.verbose:
+    parser.error("--parsable is meaningless without -v")
+
+try:
+    at = locker.read_attachtab()
+except locker.LockerError as e:
+    sys.exit(e)
+
+# Save the output for later printing
+output={'quotas': [],
+        'parsable': [],
+        'overage': []}
+
+# Squash duplicates and create a list of mountpoints
+filesystems=[]
+for x in set(options.filesys):
+    if '/' in x:
+        filesystems.append(x)
+        continue
+    if x not in at:
+        # See NOTES[6]
+        print >>sys.stderr, "%s: Not attached." % (x,)
+        print >>sys.stderr, "%s: Unknown filesystem %s." % (sys.argv[0], x)
+        sys.exit(1)
+    filesystems.append(at[x].mountpoint)
+
+for l in at:
+    logger.debug("Considering %s...", l)
+    if len(options.filesys) > 0:
+        # If they gave us a filesystem...
+        if l not in filesystems:
+            # ... and this locker doesn't match it (either mountpoint or
+            # locker name), move on
+            logger.debug("...skipping, not requested.")
+            continue
+    else:
+        # If they didn't pass any specific filesystems...
+        if not options.all_filesys and not os.access(l, os.W_OK):
+            # ...and didn't pass -a and they don't have write
+            # permissions to the locker, skip it
+            logger.debug("...skipping, not writable.")
+            continue
+
+    try:
+        quota = at[l].getQuota()
+    except locker.LockerNotSupportedError as e:
+        logger.debug("...locker not supported.")
+        continue
+    except locker.LockerError as e:
+        logger.debug("Exception while getting quota: %s", e)
+        if options.verbose:
+            # The old quota only displayed errors in verbose mode.
+            output['quotas'].append(e.message)
+        continue
+
+    pct = quota.percentage()
+    logger.debug("Usage: %d%%", pct)
+    output['quotas'].append("%-16s %8s %8s %8s %s" % \
+                                (l, quota['usage'],
+                                 quota['max'], quota['max'],
+                                 '<<' if pct >= 90 else ''))
+    output['parsable'].append("%s %s %s %s" % (l, quota['usage'], quota['max'], pct))
+    if pct >= 90:
+        output['overage'].append("%d%% of the disk quota on %s has been used." % (pct, l))
+
+if options.verbose:
+    if options.parsable:
+        print "\n".join(output['parsable'])
+        sys.exit(0)
+    uid = os.getuid()
+    try:
+        username = pwd.getpwuid(uid).pw_name
+    except Exception as e:
+        logger.debug("Exception while getting username: %s", e)
+        username = "unknown user"
+    print "Disk quotas for %s (uid %d)" % (username, uid)
+    print "%-16s %8s %8s %8s    %8s %8s %8s" % ("Filesystem",
+                                                "usage", "quota",
+                                                "limit", "files",
+                                                "quota", "limit")
+    print "\n".join(output['quotas'])
+# We always print a blank line.
+print ''
+if len(output['overage']) > 0:
+    print "\n".join(output['overage'])
+
+sys.exit(0)

home help back first fref pref prev next nref lref last post