#!/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()