diff options
Diffstat (limited to 'raidguessfs.py')
-rwxr-xr-x | raidguessfs.py | 351 |
1 files changed, 351 insertions, 0 deletions
diff --git a/raidguessfs.py b/raidguessfs.py new file mode 100755 index 0000000..ffb0912 --- /dev/null +++ b/raidguessfs.py @@ -0,0 +1,351 @@ +#!/usr/bin/env python + +# RaidGuessFS, a FUSE pseudo-filesystem to guess RAID parameters of a damaged device +# Copyright (C) 2015 Ludovic Pouzenc <ludovic@pouzenc.fr> +# +# Inspired by various python-fuse examples : +# hello.py +# Copyright (C) 2006 Andrew Straw <strawman@astraw.com> +# +# nullfs.py +# Copyright (C) 2001 Jeff Epler <jepler@unpythonic.dhs.org> +# Copyright (C) 2006 Csaba Henk <csaba.henk@creo.hu> +# +# 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 <http://www.gnu.org/licenses/> + +# Standard modules +import sys, os, errno, logging, re, fuse +# Custom modules +import mystat, mydisks, mybinview, myraid + +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.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_chunk_size', 'raid_disk_order', + 'bmp_height', 'bmp_width', 'bmp_start_offset' + ] + + self.settings_getters = [ + self.d.get_disk_count, + self.raid.get_raid_start, self.raid.get_raid_chunk_size, self.raid.get_raid_disk_order_str, + self.bmp.get_bmp_height, self.bmp.get_bmp_width, self.bmp.get_bmp_start_offset + ] + + self.settings_updaters = [ + self.update_disk_count, + self.update_raid_start, self.update_raid_chunk_size, self.update_raid_disk_order, + self.update_bmp_height, self.update_bmp_width, self.update_bmp_start_offset + ] + + self.dentries = { + '/' : [ fuse.Direntry(name) for name in ['config','disk','raid','visual'] ], + '/config': [ fuse.Direntry(name) for name in self.settings ], + '/raid' : [ fuse.Direntry(name) for name in self.raid.raid_types ], + '/disk' : [ ], # Filled in _refresh_disk_dentries() + '/visual': [ ], # Filled in _refresh_disk_dentries() + } + + for raid_type in self.raid.raid_types: + self.dentries.update( { + # TODO : all type of raid don't need the same pseudo files + '/raid/%s'%raid_type: [ fuse.Direntry(name) for name in ['result','data_xor','parity'] ], + } + ) + 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 self.raid.raid_types: + self.fattr['/raid/%s/data_xor'%raid_type].st_size = 0 # self.raid.raid_size + self.fattr['/raid/%s/parity'%raid_type].st_size = min(self.d.disks_size) / self.raid.raid_sector_size * 16 + self.fattr['/raid/%s/result'%raid_type].st_size = self.raid.raid_size + + 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) + 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_raid_end(min(self.d.disks_size)-1) + self.update_raid_disk_order(range(i)) + self._refresh_raid_fattr() + + 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_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 = arg.split() + else: + l = arg + # TODO : sanity checks (every disk number below disk count, len(list) below disk count, no double...) + logging.debug("==> %s (%d/%d)"%(l,len(l),self.d.disk_count)) + self.raid.set_raid_disk_order(l) + logging.debug("Exit. update_raid_disk_order(%s)"%arg) + + +######################################################## +# 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 stupidly + 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 + } + for raid_type in self.raid.raid_types: + self.fattr.update( { + '/raid/%s/data_xor'%raid_type: self.st.make_fake_file(0), + '/raid/%s/parity'%raid_type: self.st.make_fake_file(0), + '/raid/%s/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.error(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 self.raid.raid_types: + if path_chuncks[4] == 'result': + return self.raid.read_data(raid_type,self.d.disks,offset,size) + if path_chuncks[4] == 'parity': + return self.raid.check_data(raid_type,self.d.disks,offset,size) + + except Exception as e: + logging.error(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) + return len(buf) + except Exception as e: + logging.error(e) + return -errno.EIO + + except Exception as e: + logging.error(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() + 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() + |