#!/usr/bin/env python2.7 # RaidGuessFS, a FUSE pseudo-filesystem to guess RAID parameters of a damaged device # Copyright (C) 2015 Ludovic Pouzenc # # Inspired by various python-fuse examples : # hello.py # Copyright (C) 2006 Andrew Straw # # nullfs.py # Copyright (C) 2001 Jeff Epler # Copyright (C) 2006 Csaba Henk # # templatefs.py # Copyright (c) 2009 Matt Giuca # # Since they use LGPL and BSD licence, I choose GPLv3 (seems okay) # This file is part of RaidGuessFS. # # RaidGuessFS is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # RaidGuessFS is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with RaidGuessFS. If not, see # Standard modules import sys, os, errno, logging, re, fuse # Custom modules import mystat, mydisks, mybinview, myraid, mytasks if not hasattr(fuse, '__version__'): raise RuntimeError, \ "your fuse-py doesn't know of fuse.__version__, probably it's too old." class RaidGuessFS(fuse.Fuse): """Main class, implementing the actual pseudo-filesystem""" def __init__(self, *args, **kwargs): logging.info("Initializing filesystem") logging.debug("Enter RaidGuessFS.__init__()") super(RaidGuessFS,self).__init__(*args, **kwargs) # Early initialisations (notably modules and the static directory tree part) self.d = mydisks.MyDisks() self.raid = myraid.MyRaid(self.d) self.bmp = mybinview.MyBinView() self.st = mystat.MyStat() self.tasks = mytasks.MyTasks(self.d) self.bmp.refresh_bmp() self.dattr = { '/': self.st.make_fake_dir() } self.fattr = { '/fsinit_has_failed': self.st.make_fake_file(0) } self.settings = [ 'disk_count', 'raid_start', 'raid_end', 'raid_chunk_size', 'raid_type', 'raid_disk_order', 'raid_layout', 'raid_subraid_count', 'bmp_height', 'bmp_width', 'bmp_start_offset', 'task_start', 'task_kill', 'task_find_files_pathlist' ] self.settings_getters = [ self.d.get_disk_count, self.raid.get_raid_start, self.raid.get_raid_end, self.raid.get_raid_chunk_size, self.raid.get_raid_type, self.raid.get_raid_disk_order_str, self.raid.get_raid_layout, self.raid.get_raid_subraid_count, self.bmp.get_bmp_height, self.bmp.get_bmp_width, self.bmp.get_bmp_start_offset, self.get_task_start, self.get_task_kill, self.tasks.get_find_files_pathlist_str, ] self.settings_updaters = [ self.update_disk_count, self.update_raid_start, self.update_raid_end, self.update_raid_chunk_size, self.update_raid_type, self.update_raid_disk_order, self.raid.set_raid_layout, self.update_raid_subraid_count, self.update_bmp_height, self.update_bmp_width, self.update_bmp_start_offset, self.tasks.task_start, self.tasks.task_kill, self.tasks.append_find_files_pathlist ] self.dentries = { '/' : [ fuse.Direntry(name) for name in ['config','disk','raid','tasks','visual'] ], '/config': [ fuse.Direntry(name) for name in self.settings ], '/raid' : [ fuse.Direntry(name) for name in ['disk_parity','disk_xor','raid_result'] ], '/tasks' : [ fuse.Direntry(name) for name in self.tasks.TASK_NAMES ], '/disk' : [ ], # Filled in _refresh_disk_dentries() '/visual': [ ], # Filled in _refresh_disk_dentries() } logging.debug("Exit. RaidGuessFS.__init__()") def _refresh_disk_dentries(self): """Internal function to update directory entries about all disks""" logging.debug("Enter _refresh_disk_dentries()") disk_dentries = [] visual_dentries = [] for d in range(self.d.disk_count): st_img = self.st.make_fake_file(self.d.disks_size[d]) st_bmp = self.st.make_fake_file(self.bmp.bmp_size) self.fattr.update( { '/disk/disk%02d.img'%d: st_img, '/visual/disk%02d.bmp'%d: st_bmp, } ) disk_dentries.append( fuse.Direntry('disk%02d.img'%d) ) visual_dentries.append( fuse.Direntry('disk%02d.bmp'%d) ) self.dentries.update( { '/disk': disk_dentries } ) self.dentries.update( { '/visual': visual_dentries } ) logging.debug("Exit. _refresh_disk_dentries()") def _refresh_raid_fattr(self): """Update the raid computed attributes after a config change""" logging.debug("Enter _refresh_raid_fattr()") self.fattr['/raid/disk_parity'].st_size = self.raid.sizeof_disk_parity() self.fattr['/raid/disk_xor' ].st_size = self.raid.sizeof_disk_xor() self.fattr['/raid/raid_result'].st_size = self.raid.sizeof_raid_result() logging.debug("Exit. _refresh_raid_fattr()") def _split_path(self,path): """Internal function to explode path for read() and write() calls""" m = re.match('/([^/]+)/([^./0-9]*)([0-9+]*)(\.[^/]+)?(?:/([^/]+))?$', path) if m: return m.groups() else: return [] def _aton(self,arg,allow_zero=True): i = int(arg) if allow_zero: if i < 0: raise ValueError("Negative value make no sense here") else: if i <= 0: raise ValueError("Non-positive value make no sense here") return i def update_disk_count(self,arg): i = self._aton(arg) self.d.open_disks(getattr(self.parser.values,'source_path'), i) self._refresh_disk_dentries() self.raid.set_raid_end(min(self.d.disks_size)-1) self.update_raid_disk_order(range(i)) def update_bmp_start_offset(self, arg): i = self._aton(arg) self.bmp.set_bmp_start_offset(i) self.bmp.refresh_bmp() self._refresh_disk_dentries() def update_bmp_width(self, arg): i = self._aton(arg,False) self.bmp.set_bmp_width(i) self.bmp.refresh_bmp() self._refresh_disk_dentries() def update_bmp_height(self, arg): i = self._aton(arg,False) self.bmp.set_bmp_height(i) self.bmp.refresh_bmp() self._refresh_disk_dentries() def update_raid_start(self, arg): i = self._aton(arg) self.raid.set_raid_start(i) self._refresh_raid_fattr() def update_raid_end(self, arg): i = self._aton(arg) self.raid.set_raid_end(i) self._refresh_raid_fattr() def update_raid_subraid_count(self, arg): i = self._aton(arg) self.raid.set_raid_subraid_count(i) self._refresh_raid_fattr() def update_raid_chunk_size(self, arg): i = self._aton(arg,False) self.raid.set_raid_chunk_size(i) def update_raid_type(self, arg): if arg in myraid.MyRaid.RAID_TYPES: self.raid.set_raid_type(arg) self._refresh_raid_fattr() else: raise ValueError('update_raid_type() : raid type could be in %s'%myraid.MyRaid.RAID_TYPES) def update_raid_disk_order(self, arg): logging.debug("Enter update_raid_disk_order()") if type(arg) is str: l = map(int,arg.split()) elif type(arg) is list: l = arg else: raise TypeError('update_raid_disk_order() wants a list or str') logging.debug("==> %s (%d/%d)"%(l,len(l),self.d.disk_count)) self.raid.set_raid_disk_order(l) self._refresh_raid_fattr() logging.debug("Exit. update_raid_disk_order()") def get_task_start(self): return 'Write a task_name in this pseudo-file to start it\n' def get_task_kill(self): return 'Write a task_name in this pseudo-file to kill it\n' ######################################################## # Actual File System operations implementation follows # ######################################################## def fsinit(self): """Make some run-time initalisations after argument parsing""" logging.info("Mounting filesystem...") # WARNING : this method is called by FUSE in a context that don't show fatal exceptions, # even with -d[ebug] flag set, so log all exceptions try: self.dattr = { path: self.st.make_fake_dir() for path in self.dentries.keys() } self.fattr = { '/config/%s'%s: self.st.make_fake_file(4096,0666) for s in self.settings } self.fattr.update( { '/tasks/%s'%s: self.st.make_fake_file(4096) for s in self.tasks.TASK_NAMES }) self.fattr.update( { '/raid/%s'%s: self.st.make_fake_file(0) for s in ['disk_parity', 'disk_xor', 'raid_result'] }) self.update_disk_count(getattr(self.parser.values,'disk_count')) self._refresh_disk_dentries() self._refresh_raid_fattr() logging.info("Mounted.") except Exception as e: logging.exception(e) def fsdestroy(self): #TODO Kill tasks and see why some python processes stay alive on fusermount -u return def getattr(self, path): logging.info("getattr: %s" % path) res = self.dattr.get(path) or self.fattr.get(path) or -errno.ENOENT logging.debug("==> " + str(res)) return res def fgetattr(self, path, fh=None): #logging.debug("fgetattr: %s (fh %s)" % (path, fh)) return self.getattr(path) def readdir(self, path, offset, dh=None): logging.info("readdir: %s (offset %s, dh %s)" % (path, offset, dh)) return self.dentries[path] def truncate(self, path, size): logging.info("truncate: %s (size %s)" % (path, size)) if path == '/config/task_find_files_pathlist': self.tasks.set_find_files_pathlist([]) elif path.startswith('/config/'): return # Ignore truncates on others pseudo config files else: return -errno.EOPNOTSUPP def ftruncate(self, path, size, fh=None): logging.info("ftruncate: %s (size %s, fh %s)" % (path, size, fh)) return self.truncate(path, size) def read(self, path, size, offset, fh=None): logging.info("read: %s (size %s, offset %s, fh %s)" % (path, size, offset, fh)) path_chuncks = self._split_path(path) if path_chuncks: try: if path_chuncks[0] == 'config': # TODO take care here idx = self.settings.index(path_chuncks[1]) return str(self.settings_getters[idx]()) + "\n" if path_chuncks[0] == 'disk': if path_chuncks[1] == 'disk': i = int(path_chuncks[2]) if 0 <= i <= self.d.disk_count: if path_chuncks[3] == '.img': return self.d.read(i,offset,size) if path_chuncks[3] == '.bmp': return self.bmp.read(self.d.disks[i],offset,size) if path_chuncks[0] == 'visual': if path_chuncks[1] == 'disk': i = int(path_chuncks[2]) if 0 <= i <= self.d.disk_count: if path_chuncks[3] == '.bmp': return self.bmp.read(self.d.disks[i],offset,size) if path_chuncks[0] == 'raid': if path_chuncks[1] == 'disk_parity': return self.raid.read_disk_parity(offset,size) if path_chuncks[1] == 'disk_xor': return self.raid.read_disk_xor(offset,size) if path_chuncks[1] == 'raid_result': return self.raid.read_raid_result(offset,size) if path_chuncks[0] == 'tasks': if path_chuncks[1] == 'find_bootsect': return self.tasks.read_find_bootsect()[offset:offset+size] if path_chuncks[1] == 'find_files': return self.tasks.read_find_files()[offset:offset+size] except Exception as e: logging.exception(e) return -errno.ENOENT logging.error("Unimplemented read of '%s' (%s)"%(path, str(path_chuncks))) return -errno.ENOENT def write(self, path, buf, offset, fh=None): logging.info("write: %s (offset %s, fh %s)" % (path, offset, fh)) path_chuncks = self._split_path(path) if path_chuncks: try: if path_chuncks[0] == 'config': # TODO take care here idx = self.settings.index(path_chuncks[1]) try: self.settings_updaters[idx](buf.rstrip()) return len(buf) except Exception as e: logging.exception(e) return -errno.EIO except Exception as e: logging.exception(e) return -errno.ENOENT logging.error("Unimplemented write of '%s' (%s)"%(path, str(path_chuncks))) return -errno.ENOENT def main(): usage = fuse.Fuse.fusage + """ RaidGuessFS is a pseudo-filesystem that allows to guess parameters and disk order of a damaged RAID devices. Takes disk image files as arguments (defaults to ./diskNN.img). Could take advantage of ddrecue logs file (metadata about unreadable sectors) """ fuse.fuse_python_api = (0, 2) LOG_FILENAME = "/tmp/raidguessfs.log" logging.basicConfig(filename=LOG_FILENAME,level=logging.DEBUG) # WARN, INFO... server = RaidGuessFS(version="%prog " + fuse.__version__, usage=usage, dash_s_do='setsingle') server.multithreaded = False # Many part of the code is not re-entrant server.parser.add_option( mountopt="source_path", metavar="PATH", default="%s"%os.getcwd(), help="Absolute path that contains source disks images (defaults to current dir)" ) server.parser.add_option( mountopt="disk_count", metavar="N", default="3", help="Number of disks to try to open at mount time (also tunable at runtime)" ) server.parse(errex=1) server.main() if __name__ == '__main__': main()