]> www.infradead.org Git - users/hch/xfsprogs.git/commitdiff
xfs_scrubbed: use getparents to look up file names
authorDarrick J. Wong <djwong@kernel.org>
Wed, 7 Aug 2024 22:54:57 +0000 (15:54 -0700)
committerDarrick J. Wong <djwong@kernel.org>
Wed, 14 Aug 2024 03:08:27 +0000 (20:08 -0700)
If the kernel tells about something that happened to a file, use the
GETPARENTS ioctl to try to look up the path to that file for more
ergonomic reporting.

Signed-off-by: Darrick J. Wong <djwong@kernel.org>
scrub/xfs_scrubbed.in

index e70da45a6fef1a2cdf015ce5a7fa81e2f31e11fe..52f0bc4636e74be028233aceb5e3f5d400452c35 100644 (file)
@@ -17,6 +17,7 @@ import errno
 import ctypes
 from concurrent.futures import ProcessPoolExecutor
 import ctypes.util
+import collections
 
 try:
        # Not all systems will have this json schema validation libarary,
@@ -169,12 +170,18 @@ class xfs_handle(ctypes.Structure):
 assert ctypes.sizeof(xfs_handle) == 24
 
 class fshandle(object):
-       def __init__(self, fd, mountpoint):
+       def __init__(self, fd, mountpoint = None):
                global libhandle
                global printf_prefix
 
                self.handle = xfs_handle()
 
+               if isinstance(fd, fshandle):
+                       # copy an existing fshandle
+                       self.mountpoint = fd.mountpoint
+                       ctypes.pointer(self.handle)[0] = fd.handle
+                       return
+
                if mountpoint is None:
                        raise Exception('fshandle needs a mountpoint')
 
@@ -231,6 +238,11 @@ class fshandle(object):
                libhandle.free_handle(buf, buflen)
                return fd
 
+       def subst(self, ino, gen):
+               '''Substitute the inode and generation components of a handle.'''
+               self.handle.ha_fid.fid_ino = ino
+               self.handle.ha_fid.fid_gen = gen
+
 def libhandle_load():
        '''Load libhandle and set things up.'''
        global libhandle
@@ -394,6 +406,170 @@ def xfs_has_rmapbt(fd):
        fcntl.ioctl(fd, XFS_IOC_FSGEOMETRY, arg)
        return arg.flags & XFS_FSOP_GEOM_FLAGS_RMAPBT != 0
 
+# getparents ioctl
+class xfs_attrlist_cursor(ctypes.Structure):
+       _fields_ = [
+               ("_opaque0",            ctypes.c_uint),
+               ("_opaque1",            ctypes.c_uint),
+               ("_opaque2",            ctypes.c_uint),
+               ("_opaque3",            ctypes.c_uint)
+       ]
+
+class xfs_getparents_rec(ctypes.Structure):
+       _fields_ = [
+               ("gpr_parent",          xfs_handle),
+               ("gpr_reclen",          ctypes.c_uint),
+               ("_gpr_reserved",       ctypes.c_uint),
+       ]
+
+xfs_getparents_tuple = collections.namedtuple('xfs_getparents_tuple', \
+               ['gpr_parent', 'gpr_reclen', 'gpr_name'])
+
+class xfs_getparents_rec_array(object):
+       def __init__(self, nr_bytes):
+               self.nr_bytes = nr_bytes
+               self.bytearray = (ctypes.c_byte * int(nr_bytes))()
+
+       def __slice_to_record(self, bufslice):
+               '''Compute the number of bytes in a getparents record that contain a null-terminated directory entry name.'''
+               rec = ctypes.cast(bytes(bufslice), \
+                               ctypes.POINTER(xfs_getparents_rec))
+               fixedlen = ctypes.sizeof(xfs_getparents_rec)
+               namelen = rec.contents.gpr_reclen - fixedlen
+
+               for i in range(0, namelen):
+                       if bufslice[fixedlen + i] == 0:
+                               namelen = i
+                               break
+
+               if namelen == 0:
+                       return
+
+               return xfs_getparents_tuple(
+                               gpr_parent = rec.contents.gpr_parent,
+                               gpr_reclen = rec.contents.gpr_reclen,
+                               gpr_name = bufslice[fixedlen:fixedlen + namelen])
+
+       def get_buffer(self):
+               '''Return a pointer to the bytearray masquerading as an int.'''
+               return ctypes.addressof(self.bytearray)
+
+       def __iter__(self):
+               '''Walk the getparents records in this array.'''
+               off = 0
+               nr = 0
+               buf = bytes(self.bytearray)
+               while off < self.nr_bytes:
+                       bufslice = buf[off:]
+                       t = self.__slice_to_record(bufslice)
+                       if t is None:
+                               break
+                       yield t
+                       off += t.gpr_reclen
+                       nr += 1
+
+class xfs_getparents(ctypes.Structure):
+       _fields_ = [
+               ("_gp_cursor",          xfs_attrlist_cursor),
+               ("gp_iflags",           ctypes.c_ushort),
+               ("gp_oflags",           ctypes.c_ushort),
+               ("gp_bufsize",          ctypes.c_uint),
+               ("_pad",                ctypes.c_ulonglong),
+               ("gp_buffer",           ctypes.c_ulonglong)
+       ]
+
+       def __init__(self, fd, nr_bytes):
+               self.fd = fd
+               self.records = xfs_getparents_rec_array(nr_bytes)
+               self.gp_buffer = self.records.get_buffer()
+               self.gp_bufsize = nr_bytes
+
+       def __call_kernel(self):
+               if self.gp_oflags & XFS_GETPARENTS_OFLAG_DONE:
+                       return False
+
+               ret = fcntl.ioctl(self.fd, XFS_IOC_GETPARENTS, self)
+               if ret != 0:
+                       return False
+
+               return self.gp_oflags & XFS_GETPARENTS_OFLAG_ROOT == 0
+
+       def __iter__(self):
+               ctypes.memset(ctypes.pointer(self._gp_cursor), 0, \
+                               ctypes.sizeof(xfs_attrlist_cursor))
+
+               while self.__call_kernel():
+                       for i in self.records:
+                               yield i
+
+class xfs_getparents_by_handle(ctypes.Structure):
+       _fields_ = [
+               ("gph_handle",          xfs_handle),
+               ("gph_request",         xfs_getparents)
+       ]
+
+       def __init__(self, fd, fh, nr_bytes):
+               self.fd = fd
+               self.records = xfs_getparents_rec_array(nr_bytes)
+               self.gph_request.gp_buffer = self.records.get_buffer()
+               self.gph_request.gp_bufsize = nr_bytes
+               self.gph_handle = fh.handle
+
+       def __call_kernel(self):
+               if self.gph_request.gp_oflags & XFS_GETPARENTS_OFLAG_DONE:
+                       return False
+
+               ret = fcntl.ioctl(self.fd, XFS_IOC_GETPARENTS_BY_HANDLE, self)
+               if ret != 0:
+                       return False
+
+               return self.gph_request.gp_oflags & XFS_GETPARENTS_OFLAG_ROOT == 0
+
+       def __iter__(self):
+               ctypes.memset(ctypes.pointer(self.gph_request._gp_cursor), 0, \
+                               ctypes.sizeof(xfs_attrlist_cursor))
+               while self.__call_kernel():
+                       for i in self.records:
+                               yield i
+
+assert ctypes.sizeof(xfs_getparents) == 40
+assert ctypes.sizeof(xfs_getparents_by_handle) == 64
+assert ctypes.sizeof(xfs_getparents_rec) == 32
+
+XFS_GETPARENTS_OFLAG_ROOT      = 1 << 0
+XFS_GETPARENTS_OFLAG_DONE      = 1 << 1
+
+XFS_IOC_GETPARENTS             = _IOWR(0x58, 62, xfs_getparents)
+XFS_IOC_GETPARENTS_BY_HANDLE   = _IOWR(0x58, 63, xfs_getparents_by_handle)
+
+def fgetparents(fd, fh = None, bufsize = 1024):
+       '''Return all the parent pointers for a given fd and/or handle.'''
+
+       if fh is not None:
+               return xfs_getparents_by_handle(fd, fh, bufsize)
+       return xfs_getparents(fd, bufsize)
+
+def fgetpath(fd, fh = None, mountpoint = None):
+       '''Return a list of path components up to the root dir of the filesystem for a given fd.'''
+       ret = []
+       if fh is None:
+               nfh = fshandle(fd, mountpoint)
+       else:
+               # Don't subst into the caller's handle
+               nfh = fshandle(fh)
+
+       while True:
+               added = False
+               for pptr in fgetparents(fd, nfh):
+                       ret.insert(0, pptr.gpr_name)
+                       nfh.subst(pptr.gpr_parent.ha_fid.fid_ino, \
+                                 pptr.gpr_parent.ha_fid.fid_gen)
+                       added = True
+                       break
+               if not added:
+                       break
+       return ret
+
 # main program
 
 def health_reports(mon_fp, fh):
@@ -427,11 +603,23 @@ def health_reports(mon_fp, fh):
                        lines = []
                buf = mon_fp.readline()
 
+def inode_printf_prefix(event):
+       '''Compute the logging prefix for this event.'''
+       global printf_prefix
+
+       if 'path' not in event:
+               return printf_prefix
+
+       if printf_prefix.endswith(os.sep):
+               return f"{printf_prefix}{event['path']}"
+
+       return f"{printf_prefix}{os.sep}{event['path']}"
+
 def log_event(event):
        '''Log a monitoring event to stdout.'''
        global printf_prefix
 
-       print(f"{printf_prefix}: {event}")
+       print(f"{inode_printf_prefix(event)}: {event}")
        sys.stdout.flush()
 
 def report_lost(event):
@@ -478,6 +666,27 @@ def handle_event(e):
 
        global log
        global repair_queue
+       global has_parent
+
+       def pathify_event(event, fh):
+               '''Come up with a directory tree path for a file event.'''
+               try:
+                       path_fd = fh.open()
+               except Exception as e:
+                       print(e, file = sys.stderr)
+                       return
+
+               try:
+                       fh2 = fshandle(fh)
+                       fh2.subst(event['inumber'], event['generation'])
+                       components = [x.decode('utf-8') for x in fgetpath(path_fd, fh2)]
+                       event['path'] = os.sep.join(components)
+               except Exception as e:
+                       # Not the end of the world if we get nothing
+                       if e.errno != errno.EOPNOTSUPP:
+                               print(e, file = sys.stderr)
+               finally:
+                       os.close(path_fd)
 
        # Use a separate subprocess to handle the repairs so that the event
        # processing worker does not block on the GIL of the repair workers.
@@ -496,6 +705,8 @@ def handle_event(e):
                return
 
        stringify_timestamp(event)
+       if event['domain'] == 'inode' and has_parent:
+               pathify_event(event, fh)
        if log:
                log_event(event)
        if event['type'] == 'lost':
@@ -534,7 +745,7 @@ def monitor(mountpoint, event_queue, **kwargs):
        sys.stdout.flush()
 
        try:
-               if want_repair:
+               if want_repair or has_parent:
                        fh = fshandle(fd, mountpoint)
                mon_fd = open_health_monitor(fd, verbose = everything)
        except OSError as e:
@@ -641,6 +852,8 @@ def repair_group(event, fd, group_type):
 
 def repair_inode(event, fd):
        '''React to a inode-domain corruption event by repairing it.'''
+       ipp = inode_printf_prefix(event)
+
        for s in event['structures']:
                type = __scrub_type(s)
                if type is None:
@@ -648,10 +861,10 @@ def repair_inode(event, fd):
                try:
                        oflags = xfs_repair_inode_metadata(fd, type,
                                      event['inumber'], event['generation'])
-                       print(f"{printf_prefix}: {s}: {report_outcome(oflags)}")
+                       print(f"{ipp}: {s}: {report_outcome(oflags)}")
                        sys.stdout.flush()
                except Exception as e:
-                       print(f"{printf_prefix}: {e}", file = sys.stderr)
+                       print(f"{ipp}: {e}", file = sys.stderr)
 
 def repair_metadata(event, fh):
        '''Repair a metadata corruption.'''