From f6d396c30d87c09b9d3a8ed424fd549b732fc996 Mon Sep 17 00:00:00 2001 From: Antonio SJ Musumeci Date: Tue, 29 Sep 2015 18:55:30 -0400 Subject: [PATCH] audit (and fix) file permissions and ownership. closes #148 --- Makefile | 9 ++- README.md | 7 ++- tools/fsck.mergerfs | 133 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+), 2 deletions(-) create mode 100755 tools/fsck.mergerfs diff --git a/Makefile b/Makefile index 39cfb7f2..b68d5f61 100644 --- a/Makefile +++ b/Makefile @@ -93,10 +93,12 @@ EXEC_PREFIX = $(PREFIX) DATAROOTDIR = $(PREFIX)/share DATADIR = $(DATAROOTDIR) BINDIR = $(EXEC_PREFIX)/bin +SBINDIR = $(EXEC_PREFIX)/sbin MANDIR = $(DATAROOTDIR)/man MAN1DIR = $(MANDIR)/man1 INSTALLBINDIR = $(DESTDIR)$(BINDIR) +INSTALLSBINDIR = $(DESTDIR)$(SBINDIR) INSTALLMAN1DIR = $(DESTDIR)$(MAN1DIR) ifeq ($(XATTR_AVAILABLE),0) @@ -116,6 +118,8 @@ $(TARGET): src/version.hpp obj/obj-stamp $(OBJ) clone: $(TARGET) $(LN) -s $< $@ +fsck.mergerfs: tools/fsck.mergerfs + changelog: $(GIT2DEBCL) --name $(TARGET) > ChangeLog @@ -145,7 +149,7 @@ clean: rpm-clean distclean: clean $(GIT) clean -fd -install: install-base install-clone install-man +install: install-base install-clone install-tools install-man install-base: $(TARGET) $(INSTALL) -v -m 0755 -D "$(TARGET)" "$(INSTALLBINDIR)/$(TARGET)" @@ -153,6 +157,9 @@ install-base: $(TARGET) install-clone: clone $(CP) -a $< "$(INSTALLBINDIR)/$<" +install-tools: fsck.mergerfs + $(INSTALL) -v -m 0755 -D "tools/$<" "$(INSTALLSBINDIR)/$<" + install-man: $(MANPAGE) $(INSTALL) -v -m 0644 -D "$(MANPAGE)" "$(INSTALLMAN1DIR)/$(MANPAGE)" diff --git a/README.md b/README.md index 1c32ccad..5928ee42 100644 --- a/README.md +++ b/README.md @@ -272,11 +272,16 @@ A B C /mnt/b/full/path/to/A ``` +# TOOLING + +* /usr/sbin/fsck.mergerfs: Provides permissions and ownership auditing and the ability to fix them. + # TIPS / NOTES -* If you don't see some directories / files you expect in a merged point be sure the user has permission to all the underlying directories. If `/drive0/a` has is owned by `root:root` with ACLs set to `0700` and `/drive1/a` is `root:root` and `0755` you'll see only `/drive1/a`. +* If you don't see some directories / files you expect in a merged point be sure the user has permission to all the underlying directories. If `/drive0/a` has is owned by `root:root` with ACLs set to `0700` and `/drive1/a` is `root:root` and `0755` you'll see only `/drive1/a`. Use `fsck.mergerfs` to audit the drive for out of sync permissions. * Since POSIX gives you only error or success on calls its difficult to determine the proper behavior when applying the behavior to multiple targets. Generally if something succeeds when reading it returns the data it can. If something fails when making an action we continue on and return the last error. * The recommended options are **defaults,allow_other**. The **allow_other** is to allow users who are not the one which executed mergerfs access to the mountpoint. **defaults** is described above and should offer the best performance. It's possible that if you're running on an older platform the **splice** features aren't available and could error. In that case simply use the other options manually. +* If write performance is valued more than read it may be useful to enable **direct_io**. * Remember that some policies mixed with some functions may result in strange behaviors. Not that some of these behaviors and race conditions couldn't happen outside **mergerfs** but that they are far more likely to occur on account of attempt to merge together multiple sources of data which could be out of sync due to the different policies. * An example: [Kodi](http://kodi.tv) and [Plex](http://plex.tv) can apparently use directory [mtime](http://linux.die.net/man/2/stat) to more efficiently determine whether or not to scan for new content rather than simply performing a full scan. If using the current default **getattr** policy of **ff** its possible **Kodi** will miss an update on account of it returning the first directory found's **stat** info and its a later directory on another mount which had the **mtime** recently updated. To fix this you will want to set **func.getattr=newest**. Remember though that this is just **stat**. If the file is later **open**'ed or **unlink**'ed and the policy is different for those then a completely different file or directory could be acted on. * Due to previously mentioned issues its generally best to set **category** wide policies rather than individual **func**'s. This will help limit the confusion of tools such as [rsync](http://linux.die.net/man/1/rsync). diff --git a/tools/fsck.mergerfs b/tools/fsck.mergerfs new file mode 100755 index 00000000..368c4d59 --- /dev/null +++ b/tools/fsck.mergerfs @@ -0,0 +1,133 @@ +#!/usr/bin/python + +# The MIT License (MIT) + +# Copyright (c) 2014 Antonio SJ Musumeci + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import argparse +import os +import xattr +import errno + + +def main(): + parser = argparse.ArgumentParser(description='audit a mergerfs mount for inconsistencies') + parser.add_argument('device',type=str,help='device') + parser.add_argument('-v','--verbose',action='store_true',help='print details of audit item') + parser.add_argument('-f','--fix',choices=['manual','newest'],help='fix policy') + + args = parser.parse_args() + + if args.fix: + args.verbose = True + + if args.fix == 'manual': + fix = manual_fix + elif args.fix == 'newest': + fix = newest_fix + else: + fix = noop_fix + + try: + controlfile = os.path.join(args.device,".mergerfs") + version = xattr.getxattr(controlfile,"user.mergerfs.version") + + for (dirname,dirnames,filenames) in os.walk(args.device): + fulldirpath = os.path.join(args.device,dirname) + check_consistancy(fulldirpath,args.verbose,fix) + for filename in filenames: + fullpath = os.path.join(fulldirpath,filename) + check_consistancy(fullpath,args.verbose,fix) + + except IOError as e: + if e.errno == errno.ENOENT: + print("%s is not a mergerfs device" % args.device) + else: + print("IOError: %s" % e.strerror) + + +def check_consistancy(fullpath,verbose,fix): + paths = xattr.getxattr(fullpath,"user.mergerfs.allpaths").split('\0') + if len(paths) > 1: + stats = [os.stat(path) for path in paths] + if stats_different(stats): + print("mismatch: %s" % fullpath) + if verbose: + print_stats(paths,stats) + fix(paths,stats) + + +def noop_fix(paths,stats): + pass + + +def manual_fix(paths,stats): + done = False + while not done: + try: + value = input('Which is correct?: ') + setstat(stats[value % len(paths)],paths) + done = True + except NameError: + print("Input error: enter a value between 0 and %d" % (len(paths)-1)) + except Exception as e: + print("%s" % e) + done = True + + +def newest_fix(paths,stats): + stats.sort(key=lambda stat: stat.st_mtime) + try: + setstat(stats[-1],paths) + except Exception as e: + print("%s" % e) + + +def setstat(stat,paths): + for path in paths: + try: + os.chmod(path,stat.st_mode) + os.chown(path,stat.st_uid,stat.st_gid); + print("setting %s > uid: %d gid: %d mode: %o" % + (path,stat.st_uid,stat.st_gid,stat.st_mode)) + except Exception as e: + print("%s" % e) + + +def stats_different(stats): + base = stats[0] + for stat in stats: + if ((stat.st_mode == base.st_mode) and + (stat.st_uid == base.st_uid) and + (stat.st_gid == base.st_gid)): + continue + return True + return False + + +def print_stats(Files,Stats): + for i in xrange(0,len(Files)): + print("- %i: %s > uid: %s; gid: %s; mode: %o" % + (i,Files[i],Stats[i].st_uid,Stats[i].st_gid,Stats[i].st_mode)) + + +if __name__ == "__main__": + main()