diff mbox series

[15/21] xfs_scrubbed: use getparents to look up file names

Message ID 173568778691.2710211.10922863184869748991.stgit@frogsfrogsfrogs (mailing list archive)
State New
Headers show
Series [01/21] xfs: create hooks for monitoring health updates | expand

Commit Message

Darrick J. Wong Dec. 31, 2024, 11:51 p.m. UTC
From: Darrick J. Wong <djwong@kernel.org>

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 |  235 ++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 230 insertions(+), 5 deletions(-)
diff mbox series

Patch

diff --git a/scrub/xfs_scrubbed.in b/scrub/xfs_scrubbed.in
index 25465128864583..a4e073b3098f7a 100644
--- a/scrub/xfs_scrubbed.in
+++ b/scrub/xfs_scrubbed.in
@@ -18,6 +18,7 @@  import ctypes
 import gc
 from concurrent.futures import ProcessPoolExecutor
 import ctypes.util
+import collections
 
 try:
 	# Not all systems will have this json schema validation libarary,
@@ -171,12 +172,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')
 
@@ -233,6 +240,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
@@ -396,6 +408,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):
@@ -429,11 +605,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):
@@ -480,6 +668,39 @@  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:
+			# Not the end of the world if we get nothing
+			if e.errno != errno.EOPNOTSUPP and e.errno != errno.ENOTTY:
+				print(f'{printf_prefix}: opening file handle: {e}', file = sys.stderr)
+			return
+
+		try:
+			fh2 = fshandle(fh)
+		except OSError as e:
+			if e.errno != errno.EOPNOTSUPP:
+				print(f'{printf_prefix}: making new file handle: {e}', file = sys.stderr)
+			os.close(path_fd)
+			return
+		except Exception as e:
+			print(f'{printf_prefix}: making new file handle: {e}', file = sys.stderr)
+			os.close(path_fd)
+			return
+
+		try:
+			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 OSError as e:
+			if e.errno != errno.EOPNOTSUPP:
+				print(f'{printf_prefix}: constructing path: {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.
@@ -498,6 +719,8 @@  def handle_event(e):
 		return
 
 	stringify_timestamp(event)
+	if event['domain'] == 'inode' and has_parent and not debug_fast:
+		pathify_event(event, fh)
 	if log:
 		log_event(event)
 	if event['type'] == 'lost':
@@ -536,7 +759,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:
@@ -653,6 +876,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:
@@ -660,10 +885,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}: {s}: {e}", file = sys.stderr)
+			print(f"{ipp}: {s}: {e}", file = sys.stderr)
 
 def repair_metadata(event, fh):
 	'''Repair a metadata corruption.'''