mergerfs.fsck 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. #!/usr/bin/env python3
  2. # Copyright (c) 2016, Antonio SJ Musumeci <trapexit@spawn.link>
  3. # Permission to use, copy, modify, and/or distribute this software for any
  4. # purpose with or without fee is hereby granted, provided that the above
  5. # copyright notice and this permission notice appear in all copies.
  6. # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  7. # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  8. # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  9. # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  10. # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  11. # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  12. # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  13. import argparse
  14. import ctypes
  15. import errno
  16. import io
  17. import os
  18. import sys
  19. _libc = ctypes.CDLL("libc.so.6",use_errno=True)
  20. _lgetxattr = _libc.lgetxattr
  21. _lgetxattr.argtypes = [ctypes.c_char_p,ctypes.c_char_p,ctypes.c_void_p,ctypes.c_size_t]
  22. def lgetxattr(path,name):
  23. if type(path) == str:
  24. path = path.encode(errors='backslashreplace')
  25. if type(name) == str:
  26. name = name.encode(errors='backslashreplace')
  27. length = 64
  28. while True:
  29. buf = ctypes.create_string_buffer(length)
  30. res = _lgetxattr(path,name,buf,ctypes.c_size_t(length))
  31. if res >= 0:
  32. return buf.raw[0:res]
  33. else:
  34. err = ctypes.get_errno()
  35. if err == errno.ERANGE:
  36. length *= 2
  37. elif err == errno.ENODATA:
  38. return None
  39. else:
  40. raise IOError(err,os.strerror(err),path)
  41. def ismergerfs(path):
  42. try:
  43. lgetxattr(path,"user.mergerfs.fullpath")
  44. return True
  45. except IOError as e:
  46. return False
  47. def setstat(stat,paths):
  48. for path in paths:
  49. try:
  50. os.chmod(path,stat.st_mode)
  51. os.chown(path,stat.st_uid,stat.st_gid);
  52. print("set %s > uid: %d gid: %d mode: %o" %
  53. (path,stat.st_uid,stat.st_gid,stat.st_mode))
  54. except Exception as e:
  55. print("%s" % e)
  56. def stats_different(stats):
  57. base = stats[0]
  58. for stat in stats:
  59. if ((stat.st_mode == base.st_mode) and
  60. (stat.st_uid == base.st_uid) and
  61. (stat.st_gid == base.st_gid)):
  62. continue
  63. return True
  64. return False
  65. def size_equal(stats):
  66. base = stats[0]
  67. for stat in stats:
  68. if stat.st_size != base.st_size:
  69. return False
  70. return True
  71. def print_stats(Files,Stats):
  72. for i in range(0,len(Files)):
  73. print(" %i: %s" % (i,Files[i].decode(errors='backslashreplace')))
  74. data = (" - uid: {0:5}; gid: {1:5}; mode: {2:6o}; "
  75. "size: {3:10}; mtime: {4}").format(
  76. Stats[i].st_uid,
  77. Stats[i].st_gid,
  78. Stats[i].st_mode,
  79. Stats[i].st_size,
  80. Stats[i].st_mtime)
  81. print (data)
  82. def noop_fix(paths,stats):
  83. pass
  84. def manual_fix(paths,stats):
  85. done = False
  86. while not done:
  87. try:
  88. value = input('Which is correct?: ')
  89. value = int(value)
  90. if((value >= len(paths)) or (value < 0)):
  91. print("Input error: enter a value [0,%d]" % (len(paths)-1))
  92. continue
  93. setstat(stats[value],paths)
  94. done = True
  95. except Exception as e:
  96. print("%s" % e)
  97. done = True
  98. def newest_fix(paths,stats):
  99. stats.sort(key=lambda stat: stat.st_mtime)
  100. try:
  101. newest = stats[-1]
  102. setstat(newest,paths)
  103. except Exception as e:
  104. print("%s" % e)
  105. def nonroot_fix(paths,stats):
  106. try:
  107. for stat in stats:
  108. if stat.st_uid != 0:
  109. setstat(stat,paths)
  110. return
  111. return newest_fix(paths,stats)
  112. except Exception as e:
  113. print("%s" % e)
  114. def getfixfun(name):
  115. if name == 'manual':
  116. return manual_fix
  117. elif name == 'newest':
  118. return newest_fix
  119. elif name == 'nonroot':
  120. return nonroot_fix
  121. return noop_fix
  122. def check_consistancy(fullpath,verbose,size,fix):
  123. paths = lgetxattr(fullpath,"user.mergerfs.allpaths")
  124. if not paths:
  125. return
  126. paths = paths.split(b'\0')
  127. if len(paths) <= 1:
  128. return
  129. stats = [os.stat(path) for path in paths]
  130. if (size and not size_equal(stats)):
  131. return
  132. if not stats_different(stats):
  133. return
  134. print("%s" % fullpath)
  135. if verbose:
  136. print_stats(paths,stats)
  137. fix(paths,stats)
  138. def buildargparser():
  139. parser = argparse.ArgumentParser(description='audit a mergerfs mount for inconsistencies')
  140. parser.add_argument('dir',type=str,
  141. help='starting directory')
  142. parser.add_argument('-v','--verbose',action='store_true',
  143. help='print details of audit item')
  144. parser.add_argument('-s','--size',action='store_true',
  145. help='only consider if the size is the same')
  146. parser.add_argument('-f','--fix',choices=['manual','newest','nonroot'],
  147. help='fix policy')
  148. return parser
  149. def main():
  150. sys.stdout = io.TextIOWrapper(sys.stdout.buffer,
  151. encoding='utf8',
  152. errors='backslashreplace',
  153. line_buffering=True)
  154. sys.stderr = io.TextIOWrapper(sys.stderr.buffer,
  155. encoding='utf8',
  156. errors='backslashreplace',
  157. line_buffering=True)
  158. parser = buildargparser()
  159. args = parser.parse_args()
  160. if args.fix:
  161. args.verbose = True
  162. fix = getfixfun(args.fix)
  163. args.dir = os.path.realpath(args.dir)
  164. if not ismergerfs(args.dir):
  165. print("%s is not a mergerfs directory" % args.dir)
  166. sys.exit(1)
  167. try:
  168. size = args.size
  169. verbose = args.verbose
  170. for (dirname,dirnames,filenames) in os.walk(args.dir):
  171. fulldirpath = os.path.join(args.dir,dirname)
  172. check_consistancy(fulldirpath,verbose,size,fix)
  173. for filename in filenames:
  174. fullpath = os.path.join(fulldirpath,filename)
  175. check_consistancy(fullpath,verbose,size,fix)
  176. except KeyboardInterrupt:
  177. pass
  178. except IOError as e:
  179. if e.errno == errno.EPIPE:
  180. pass
  181. else:
  182. raise
  183. sys.exit(0)
  184. if __name__ == "__main__":
  185. main()