From 5943acb92ce0159e9f482748e4fa4aadddae6851 Mon Sep 17 00:00:00 2001 From: Ludovic Pouzenc Date: Fri, 12 Jun 2015 08:34:26 +0200 Subject: Initial import. RAID 5 xor parity checking is working, data reading on left-assymetric RAID 5 is working. Nothing done on other RAID types. --- mybinview.py | 96 +++++++++++++++ mydisks.py | 75 ++++++++++++ myraid.py | 154 ++++++++++++++++++++++++ mystat.py | 57 +++++++++ raidguessfs.py | 351 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ tools/pattern.py | 12 ++ 6 files changed, 745 insertions(+) create mode 100644 mybinview.py create mode 100644 mydisks.py create mode 100644 myraid.py create mode 100644 mystat.py create mode 100755 raidguessfs.py create mode 100755 tools/pattern.py diff --git a/mybinview.py b/mybinview.py new file mode 100644 index 0000000..5064013 --- /dev/null +++ b/mybinview.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python + +# RaidGuessFS, a FUSE pseudo-filesystem to guess RAID parameters of a damaged device +# Copyright (C) 2015 Ludovic Pouzenc +# +# 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 + +import struct + +class MyBinView(): + """"Auxiliary class, dumping binary data as image files""" + + def __init__(self, *args, **kwargs): + self.bmp_start_offset=0 + self.bmp_height=4096 + self.bmp_width=1024 + + self.bmp_pixeldata_start=0 + self.bmp_size=0 + self.bmp_header=bytearray() + + def get_bmp_start_offset(self): + return self.bmp_start_offset + + def get_bmp_height(self): + return self.bmp_height + + def get_bmp_width(self): + return self.bmp_width + + def set_bmp_start_offset(self, new_bmp_start_offset): + """Offset to choose which fiel part you want to render as image""" + self.bmp_start_offset = new_bmp_start_offset + + def set_bmp_width(self, new_bmp_width): + """Update the BMP (yes bitmap images) width in pixels""" + self.bmp_width = new_bmp_width / 4 * 4 + self.refresh_bmp() + self.refresh_disks_dentries() + + def set_bmp_height(self, new_bmp_height): + """Update the BMP (yes bitmap images) height in pixels""" + self.bmp_height = new_bmp_height + self.refresh_bmp() + self.refresh_disks_dentries() + + def refresh_bmp(self): + """Update the BMP headers accroding to bitmap settings""" + bmp_header_fmt = struct.Struct('< 2s I x x x x I I i i H H I I i i I I') + + bitmap_pixel_count = self.bmp_width * self.bmp_height + # From https://en.wikipedia.org/wiki/BMP_file_format + bitmap_magic='BM' + dib_size=40 # BITMAPINFOHEADER (Windows NT, 3.1x or later) + bitmap_bpp=8 + bitmap_compress=0 + imagehres=2835 # pixels per meter + imagevres=2835 + palette_size=256 + palette_start = bmp_header_fmt.size + + self.bmp_pixeldata_start = palette_start + palette_size*4 + self.bmp_size = self.bmp_pixeldata_start + bitmap_pixel_count + self.bmp_header = bytearray(self.bmp_pixeldata_start) + bmp_header_fmt.pack_into(self.bmp_header,0, + 'BM', self.bmp_size, self.bmp_pixeldata_start, dib_size, + self.bmp_width, self.bmp_height, 1, bitmap_bpp, bitmap_compress, + bitmap_pixel_count, imagehres, imagevres, palette_size, palette_size + ) + + for i in range(palette_size): + struct.pack_into('< B B B B', self.bmp_header, bmp_header_fmt.size + i*4, i,i,i,0) # All shades of gray in RGBA + + def read(self, fd, offset, size): + if offset < self.bmp_pixeldata_start: + data = str(self.bmp_header[offset:offset+size-1]) + if offset + size > self.bmp_pixeldata_start: + data += self.read(fd, self.bmp_pixeldata_start, size - self.bmp_pixeldata_start) + return data + else: + fd.seek(self.bmp_start_offset + offset - self.bmp_pixeldata_start) + return fd.read(size) + diff --git a/mydisks.py b/mydisks.py new file mode 100644 index 0000000..c3b7716 --- /dev/null +++ b/mydisks.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python + +# RaidGuessFS, a FUSE pseudo-filesystem to guess RAID parameters of a damaged device +# Copyright (C) 2015 Ludovic Pouzenc +# +# 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 + +import os, sys, logging + +class MyDisks(): + """Auxiliary class, managing disk layer""" + + def __init__(self, *args, **kwargs): + self.disks = [] + self.disk_paths = [] + self.disk_count = 0 + self.disks_size = [0] + self.max_disks = 16 + + def get_disk_count(self): + return self.disk_count + + def set_disks_path(self,disk_path_list): + """Set the list of real filepath to disks or images and (re)open them""" + self.disk_paths = disk_path_list + + def set_disk_count(self,new_disk_count): + """Update the number of attached disks on the fly""" + self.disk_count = min(new_disk_count,self.max_disks) + logging.info("MyDisks.set_disk_count(%d) : setting disk_count to %d" % (new_disk_count, self.disk_count)) + + def open_disks(self): + """(re)open all disks""" + logging.debug("Enter open_disks()") + for fd in self.disks: + try: + fd.close() + except: + pass + self.disks = [ None for d in range(self.disk_count) ] + self.disks_size = [ 0 for d in range(self.disk_count) ] + + for d in range(self.disk_count): + path = self.disk_paths[d] + logging.debug("Try to open disk #%2d"%d) + try: + self.disks[d] = open(path, "r") + self.disks_size[d] = os.lstat(path).st_size + logging.debug("Opened disk #%2d"%d) + except IOError as e: + logging.error("Can't open disk #%2d ('%s') : %s"%(d, path, e.strerror)) + self.disks_size[d] = 0 + except: + logging.error("Can't open disk #%2d ('%s') : unhandled exception"%(d, path)) + self.disks_size[d] = 0 + logging.debug("Exit. open_disks()") + + def read(self,disk_no,offset,size): + self.disks[disk_no].seek(offset) + return self.disks[disk_no].read(size) + + diff --git a/myraid.py b/myraid.py new file mode 100644 index 0000000..60e8b28 --- /dev/null +++ b/myraid.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python + +# RaidGuessFS, a FUSE pseudo-filesystem to guess RAID parameters of a damaged device +# Copyright (C) 2015 Ludovic Pouzenc +# +# 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 + +import logging, numpy + +class MyRaid(): + """Auxiliary class, managing RAID layer""" + + def __init__(self, *args, **kwargs): + self.raid_start = 0 + self.raid_end = 0 + self.raid_size = 0 + self.raid_sector_size = 512 + self.raid_chunk_size = 65536 + self.raid_disk_order = range(15) + self.raid_types = [ '0', '1', '5', '5+0' ] + + def _update_raid_size(self): + if self.raid_end > self.raid_start: + self.raid_size = self.raid_end - self.raid_start + else: + self.raid_size = 0 + + def get_raid_start(self): + return self.raid_start + + def get_raid_chunk_size(self): + return self.raid_chunk_size + + def get_raid_disk_order(self): + return self.raid_disk_order + + def get_raid_disk_order_str(self): + return ' '.join(map(str,self.raid_disk_order)) + + def set_raid_start(self, new_raid_start): + """Update the start offset of raid data on underlying disks""" + self.raid_start = new_raid_start + self._update_raid_size() + + def set_raid_end(self, new_raid_end): + """Update the end offset of raid data on underlying disks""" + self.raid_end = new_raid_end + self._update_raid_size() + + def set_raid_chunk_size(self, new_raid_chunk_size): + """Update the size of chucks of data (or slice size)""" + self.raid_chunk_size = new_raid_chunk_size + + def set_raid_disk_order(self, new_raid_disk_order): + """Update the raid logical disk order""" + self.raid_disk_order = new_raid_disk_order + + + def read_data(self,raid_type,disks,offset,size): + """TODO""" + disk_count = len(self.raid_disk_order) + + # This code is RAID 5 only + + slice_no = offset / self.raid_chunk_size + slice_off = offset % self.raid_chunk_size + segment=slice_no/(disk_count-1) + par_disk=(disk_count-1) - (segment % disk_count) # TODO : equivalent a : segment-1 % disk_count ? + data_disk=( par_disk + 1 + (slice_no % (disk_count-1)) ) % disk_count + off_disk = self.raid_start + segment * self.raid_chunk_size + slice_off + + size2 = min(size, (slice_no+1) * self.raid_chunk_size - offset) + + logging.info("raid.read_data(%s): offset=%d,slice_no=%d,slice_off=%d,segment=%d,par_disk=%d,data_disk=%d,off_disk=%d,size2=%d,slice_off+size2=%d" + % (raid_type,offset,slice_no,slice_off,segment,par_disk,data_disk,off_disk,size2,slice_off+size2) ) + + data_fd = disks[self.raid_disk_order[data_disk]] + data_fd.seek(off_disk) + data = data_fd.read(size2) + + # This kills performance but don't make short reads before EOF + #if size2 < size: + # data += self.read_data(self,raid_type,disks,offset+size2,size-size2) + + return data + + def xor_blocks(self,fd_list, offset, size): + """TODO""" + logging.info("Enter xor_blocks(fd_list,%d,%d)"%(offset, size)) + + assert(size % 8 == 0), "Size must be multiple of 8" + dt = numpy.dtype(' +# +# 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 + +import stat, os, time, copy, fuse + +class MyStat(fuse.Stat): + """Handy class to produce fake file or dir attributes""" + + def __init__(self): + self.st_mode = stat.S_IFDIR | 0555 + self.st_ino = 0 + self.st_dev = 0 + self.st_nlink = 2 + self.st_uid = os.getuid() + self.st_gid = os.getgid() + self.st_size = 0 + self.st_atime = time.time() + self.st_mtime = self.st_atime + self.st_ctime = self.st_atime + + def __str__(self): + return str(self.__dict__) + + def make_fake_dir(self): + return copy.copy(self) + + def make_fake_file(self,size,mode=0444): + st = copy.copy(self) + st.st_size = size + st.st_mode = stat.S_IFREG | mode + st.st_nlink = 1 + return st + +#def main(): +# st = MyStat() +# print st.make_fake_dir() +# print st.make_fake_file(12) +# +#if __name__ == '__main__': +# main() 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 +# +# 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 + +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() + diff --git a/tools/pattern.py b/tools/pattern.py new file mode 100755 index 0000000..0367698 --- /dev/null +++ b/tools/pattern.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +def main(): + i=0 + max=pow(10,15) + while True: + print "%015d"%i, + i=(i+1)%max + + +if __name__ == '__main__': + main() -- cgit v1.2.3