#!/usr/bin/env python # 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 the static directory tree part) self.d = mydisks.MyDisks() self.raid = myraid.MyRaid() 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_disk_order', 'raid_layout', '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_disk_order_str, self.raid.get_raid_layout, 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_disk_order, self.raid.set_raid_layout, self.update_bmp_height, self.update_bmp_width, self.update_bmp_start_offset, self.tasks.task_start, self.tasks.task_kill, self.update_task_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 myraid.MyRaid.RAID_TYPES ], '/tasks' : [ fuse.Direntry(name) for name in self.tasks.TASK_NAMES ], '/disk' : [ ], # Filled in _refresh_disk_dentries() '/visual': [ ], # Filled in _refresh_disk_dentries() } for raid_type in myraid.MyRaid.RAID_TYPES: self.dentries.update( { '/raid/%s'%raid_type: [ fuse.Direntry(name) for name in ['disk_parity','disk_xor','raid_result'] ], } ) 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()") for raid_type in myraid.MyRaid.RAID_TYPES: self.fattr['/raid/%s/disk_parity'%raid_type].st_size = self.raid.sizeof_disk_parity(raid_type) self.fattr['/raid/%s/disk_xor' %raid_type].st_size = self.raid.sizeof_disk_xor (raid_type) self.fattr['/raid/%s/raid_result'%raid_type].st_size = self.raid.sizeof_raid_result(raid_type) 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 update_disk_count(self,arg): i = int(arg) #TODO : raise exception instead of using assertions assert (i > 0), "Negative value make no sense here" self.d.set_disk_count(i) self.d.open_disks() self._refresh_disk_dentries() self.raid.set_disks(self.d.disks) 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 = int(arg) assert (i >= 0), "Negative value make no sense here" self.bmp.set_bmp_start_offset(i) self.bmp.refresh_bmp() self._refresh_disk_dentries() def update_bmp_width(self, arg): i = int(arg) assert (i > 0), "Non-positive value make no sense here" self.bmp.set_bmp_width(i) self.bmp.refresh_bmp() self._refresh_disk_dentries() def update_bmp_height(self, arg): i = int(arg) assert (i > 0), "Non-positive value make no sense here" self.bmp.set_bmp_height(i) self.bmp.refresh_bmp() self._refresh_disk_dentries() def update_raid_start(self, arg): i = int(arg) assert (i >= 0), "Negative value make no sense here" self.raid.set_raid_start(i) self._refresh_raid_fattr() def update_raid_end(self, arg): i = int(arg) assert (i >= 0), "Negative value make no sense here" self.raid.set_raid_end(i) self._refresh_raid_fattr() def update_raid_chunk_size(self, arg): i = int(arg) assert (i > 0), "Non-positive value make no sense here" self.raid.set_raid_chunk_size(i) def update_raid_disk_order(self, arg): logging.debug("Enter update_raid_disk_order(%s)"%arg) 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(%s)"%arg) def update_task_find_files_pathlist(self, arg): logging.debug("Enter update_task_find_files_pathlist(%s)"%arg) if type(arg) is str: l = arg.split() elif type(arg) is list: l = arg else: raise TypeError('update_task_find_files_pathlist() wants a list or str') # FIXME : write() is usually called after each end line, so the previous path are destroyed self.tasks.set_find_files_pathlist(l) logging.debug("Exit. update_task_find_files_pathlist(%s)"%arg) 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(64,0666) for s in self.settings } self.fattr.update( { '/tasks/%s'%s: self.st.make_fake_file(512) for s in self.tasks.TASK_NAMES }) for raid_type in myraid.MyRaid.RAID_TYPES: self.fattr.update( { '/raid/%s/disk_parity'%raid_type: self.st.make_fake_file(0), '/raid/%s/disk_xor'%raid_type: self.st.make_fake_file(0), '/raid/%s/raid_result'%raid_type: self.st.make_fake_file(0), }) self.d.set_disks_path([getattr(self.parser.values,'disk%02d'%d) for d in range(self.d.max_disks)]) self.update_disk_count(len(self.d.disk_paths)) self._refresh_disk_dentries() self._refresh_raid_fattr() logging.info("Mounted.") except Exception as e: logging.exception(e) 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.startswith('/config/'): return # Ignore truncates on 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': raid_type=path_chuncks[2] if raid_type in myraid.MyRaid.RAID_TYPES: if path_chuncks[4] == 'disk_parity': return self.raid.read_disk_parity(raid_type,offset,size) if path_chuncks[4] == 'disk_xor': return self.raid.read_disk_xor(raid_type,offset,size) if path_chuncks[4] == 'raid_result': return self.raid.read_raid_result(raid_type,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 = "raidguessfs.log" #logging.basicConfig(filename=LOG_FILENAME,level=logging.WARN,) #logging.basicConfig(filename=LOG_FILENAME,level=logging.INFO,) logging.basicConfig(filename=LOG_FILENAME,level=logging.DEBUG,) server = RaidGuessFS(version="%prog " + fuse.__version__,usage=usage,dash_s_do='setsingle') server.multithreaded = False cwd = os.getcwd() # TODO : only 2 parameters, but taking a parametrized string for num in range(server.d.max_disks): server.parser.add_option( mountopt="disk%02d"%num, metavar="ABS_PATH", default="%s/disk%02d.img"%(cwd,num), help="Disk #%d image file path [default: ./disk%02d.img]"%(num,num) ) server.parser.add_option( mountopt="logf%02d"%num, metavar="ABS_PATH", default="%s/disk%02d.log"%(cwd,num), help="Disk #%d ddrescue log file [default: ./disk%02d.log]"%(num,num) ) server.parse(errex=1) server.main() if __name__ == '__main__': main()