#!/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 import myraidmaths class MyRaid(): """Auxiliary class, managing RAID layer""" RAID_TYPES = [ '0', '1', '5', '5+0' ] RAID5_LAYOUTS = [ 'la', 'ra', 'ls', 'rs' ] def __init__(self, mydisks): self.d = mydisks self.raid_start = 0 self.raid_end = 0 self.raid_chunk_size = 65536 self.raid_disk_order = [] self.raid_disk_count = 0 self.raid_type = '5' self.raid_layout = 'ls' self.raid_disks = [] self.raid_subraid_count = 2 def get_raid_start(self): return self.raid_start def get_raid_end(self): return self.raid_end 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 get_raid_type(self): return self.raid_type def get_raid_layout(self): return self.raid_layout def get_raid_subraid_count(self): return self.raid_subraid_count def set_raid_start(self, new_raid_start): """Update the start offset of raid data on underlying disks""" self.raid_start = new_raid_start def set_raid_end(self, new_raid_end): """Update the end offset of raid data on underlying disks""" self.raid_end = new_raid_end 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""" check=[0] * self.d.disk_count for item in new_raid_disk_order: d = int(item) if not 0 <= d < self.d.disk_count: raise ValueError('Value out of range : %i [0,%i]'%(d,self.d.disk_count-1)) check[d]=check[d]+1 for d in range(self.d.disk_count): if check[d] != 1 and check[d] != 0: raise ValueError('Disk %i appears %i times (must be 0 or 1)'%(d,check[d])) self.raid_disk_count = len(new_raid_disk_order) self.raid_disk_order = new_raid_disk_order self.raid_disks = [ self.d.disks[i] for i in self.raid_disk_order ] def set_raid_type(self, new_raid_type): if new_raid_type in MyRaid.RAID_TYPES: self.raid_type = new_raid_type else: raise ValueError('raid_type has to be one of %s'%' '.join(MyRaid.RAID_TYPES)) def set_raid_subraid_count(self, new_raid_subraid_count): """Update the number of subcomponents in nested RAID levels""" self.raid_subraid_count = new_raid_subraid_count def set_raid_layout(self, new_raid_layout): """Update the kind of data/parity block layout for RAID5 family""" if new_raid_layout in MyRaid.RAID5_LAYOUTS: self.raid_layout = new_raid_layout else: raise ValueError('raid_layout has to be one of %s'%' '.join(MyRaid.RAID_LAYOUTS)) def sizeof_raid_result(self): size = max(0, self.raid_end - self.raid_start) return { '0' : size * self.raid_disk_count, '1' : size if self.raid_disk_count == 2 else 0, '5' : size * (self.raid_disk_count - 1) if self.raid_disk_count >= 3 else 0, '5+0': size * (self.raid_disk_count - 2) if self.raid_disk_count >= 6 and self.raid_disk_count % 2 == 0 else 0, }[self.raid_type] def sizeof_disk_xor(self): return max(0, self.raid_end - self.raid_start) def sizeof_disk_parity(self): size = max(0, self.raid_end - self.raid_start) / self.d.sector_size * 16 return { '0' : 64, '1' : size if self.raid_disk_count == 2 else 64, '5' : size if self.raid_disk_count >= 3 else 64, '5+0': size if self.raid_disk_count >= 6 and self.raid_disk_count % 2 == 0 else 64, }[self.raid_type] def read_disk_xor(self,offset,size): """Returns raw bitwise XOR against a bunch of disks slice""" return myraidmaths.MyRaidMaths.xor_blocks(self.raid_disks,offset,size)[1] def read_disk_parity(self,offset,size): """Returns textual information about parity status of each sector""" logging.warn("Enter read_disk_parity(%d,%d)"%(offset,size)) msg = { '0' : 'There no notion of parity in RAID 0 mode\n', '1' : None if self.raid_disk_count == 2 else 'Wrong disk count (should be 2)\n', '5' : None if self.raid_disk_count >= 3 else 'Wrong disk count (should be >=3)\n', '5+0': None if self.raid_disk_count >= 6 and self.raid_disk_count % 2 == 0 else 'Wrong disk count (should be >=6 and even)\n', }[self.raid_type] if msg: return msg[offset:offset+size] start = self.raid_start + offset * self.d.sector_size / 16 end = start + size * self.d.sector_size / 16 #TODO : improove for nested levels if self.raid_type in ['1','5', '5+0']: result = ''.join( [ '0x%011x %c\n'%( addr, myraidmaths.MyRaidMaths.xor_blocks(self.raid_disks, addr, self.d.sector_size)[0]) for addr in xrange(start, end, self.d.sector_size) ]) else: result = None logging.warn("Exit. read_disk_parity(%d,%d)"%(offset,size)) return result def read_raid_result(self,offset,size): """Returns actual RAID data""" res = myraidmaths.MyRaidMaths.apply_raid_layout(offset, size, self.raid_type, self.raid_layout, self.raid_chunk_size, self.raid_disk_count, self.raid_start, self.raid_subraid_count); (segment_no, segment_off, stripe_no, subraid_no, par_disk, data_disk, off_disk, aligned_read_size) = res logging.debug("raid.read_result(%s): offset=%d,segment_no=%d,segment_off=%d,stripe_no=%d,subraid_no=%d,par_disk=%d(disk%02d),data_disk=%d(disk%02d),off_disk=%d,aligned_read_size=%d,segment_off+aligned_read_size=%d" % (self.raid_type,offset,segment_no,segment_off,stripe_no,subraid_no,par_disk,self.raid_disk_order[par_disk],data_disk,self.raid_disk_order[data_disk],off_disk,aligned_read_size,segment_off+aligned_read_size) ) data_fd = self.raid_disks[data_disk] if self.d.is_readable(self.raid_disk_order[data_disk],off_disk,aligned_read_size): # No damaged sectors until the end of the chunck, so just read the data disk data_fd.seek(off_disk) data = data_fd.read(aligned_read_size) else: # TODO : this is only for RAID5 logging.warn('Try to recovering damaged chunck (raid_offset: 0x%011x, data_disk: %i, disk_offset: 0x%011x' % (offset, self.raid_disk_order[data_disk], off_disk) ) # Damaged sectors, check / recover every sector other_disks = list(self.raid_disk_order) other_disks.remove(self.raid_disk_order[data_disk]) other_fds = list(self.raid_disks) other_fds.remove(data_fd) data_arr = [] for s in xrange(off_disk, off_disk+aligned_read_size, self.d.sector_size): if self.d.is_readable(self.raid_disk_order[data_disk],s,self.d.sector_size): # Current sector is readable from data disk, read it logging.debug('-> 0x%011x : readable'%s) data_fd.seek(off_disk) data_arr.append(data_fd.read(self.d.sector_size)) else: # Current sector is dead on data disk, recover it if possible recoverable = reduce(lambda a,b: a and b, [ self.d.is_readable(other_disk,off_disk,self.d.sector_size) for other_disk in other_disks ]) if recoverable: logging.info('-> 0x%011x : recoverable'%s) data_arr.append( myraidmaths.MyRaidMaths.xor_blocks(other_fds, s,self.d.sector_size)[1] ) else: logging.warn('-> 0x%011x : unrecoverable'%s) data_arr.append( '\0' * self.d.sector_size) data = ''.join(data_arr) # Prevent short reads, seems mandatory for losetup'ing raid_result but kills performance #TODO : make it activable per config if aligned_read_size < size: return ''.join( (data, self.read_raid_result(offset + aligned_read_size, size - aligned_read_size) ) ) return data