summaryrefslogtreecommitdiff
path: root/raidguessfs.py
diff options
context:
space:
mode:
authorLudovic Pouzenc <lpouzenc@lud-GB1>2015-06-12 08:34:26 +0200
committerLudovic Pouzenc <lpouzenc@lud-GB1>2015-06-12 08:34:26 +0200
commit5943acb92ce0159e9f482748e4fa4aadddae6851 (patch)
treee4fc66dabda439f8a98535f10287018e871c2e41 /raidguessfs.py
parent49c830d2d70547c31cab1b1f0bc9f26d77d62a7e (diff)
downloadraidguessfs-5943acb92ce0159e9f482748e4fa4aadddae6851.tar.gz
raidguessfs-5943acb92ce0159e9f482748e4fa4aadddae6851.tar.bz2
raidguessfs-5943acb92ce0159e9f482748e4fa4aadddae6851.zip
Initial import.
RAID 5 xor parity checking is working, data reading on left-assymetric RAID 5 is working. Nothing done on other RAID types.
Diffstat (limited to 'raidguessfs.py')
-rwxr-xr-xraidguessfs.py351
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()
+