[27548] in Source-Commits
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)