From: darkk <http://darkk.livejournal.com>
Date: Mon, 26 Nov 2007 18:21:07 +0000 (UTC)
Subject: Восстановление ext2/ext3 раздела при помощи debugfs.ext2
Оригинал: http://darkk.livejournal.com/28545.html
Дано: битый раздел, на котором лежит N месяцев работы. Бэкапов нет.
e2fsck теряет волю, ситуацию усугубляет растущее кол-во badblock-ов на
винте.
livecd storage # e2fsck -n /dev/hdb1
e2fsck 1.38 (30-Jun-2005)
Superblock has an invalid ext3 journal (inode 8).
Clear? no
e2fsck: Illegal inode number while checking ext3 journal for /dev/hdb1
При попытке сказать yes - e2fsck падал по подобию assert(), когда искал root, к
орневая директория была куском нулей
livecd storage # debugfs -c /dev/hdb1
debugfs 1.38 (30-Jun-2005)
/dev/hdb1: catastrophic mode - not reading inode or group bitmaps
debugfs: show_super_stats -h
...
Inode count: 7340032
Block count: 14659304
...
Узнаем свободные inode и получаем листинг условно-живых директорий
Крайне полезно этот ls.out залить в sql-базу (например так), чтобы
шустро собирать по нему статистику... У меня размер основной таблицы
entries (inode, name, curdir, size) индексированной базы (схема
прилагается) получился под 100 мегабайт с 60гигового раздела.
Затем можно пополнить базу следующими (НЕ абсолютно верными)
наблюдениями:
INSERT INTO directories SELECT inode, curdir FROM entries
GROUP BY inode HAVING COUNT(*) > 1;
INSERT INTO inodes SELECT inode, COUNT(*) FROM entries GROUP BY inode;
INSERT INTO broken_dirs SELECT entries.inode FROM entries
LEFT JOIN entries AS dirs ON (dirs.inode = entries.inode AND dirs.name = '.')
WHERE entries.name = '..' AND dirs.name IS NULL GROUP BY entries.inode;
INSERT INTO hardlinks SELECT inodes.inode FROM inodes
LEFT JOIN entries ON (inodes.inode = entries.inode AND (entries.name IN ('.', '..')))
WHERE nlink > 1 AND entries.name IS NULL;
Еще кстати будет перелить раздел на новый винт, который не сыпется с
околозвуковой скоростью, при том в двух экземплярах, один для
экспериментов, второй на всякий случай
Теперь ищем детей / (inode-2) и возвращаем их на место
mysql> select * from entries where inode = 2;
+-------+---------+------+------+
| inode | curdir | name | size | # комментарий, изходя из содержания
+-------+---------+------+------+
| 2 | 2392065 | .. | 12 | /var
| 2 | 6275073 | .. | 12 | /home
...
+-------+---------+------+------+
17 rows in set (0.00 sec)
mysql> select * from entries where curdir = 2392065;
+---------+---------+---------+------+
| inode | curdir | name | size |
+---------+---------+---------+------+
| 2392065 | 2392065 | . | 12 |
| 2 | 2392065 | .. | 12 |
| 2392066 | 2392065 | lock | 12 |
| 2392067 | 2392065 | run | 12 |
| 2392068 | 2392065 | backups | 16 |
| 2392069 | 2392065 | cache | 16 |
... # да, это явно /var
+---------+---------+---------+------+
15 rows in set (0.03 sec)
А потом устраиваем большое переименование /lost+found
livecd ~ # ls /mnt/slave/lost+found | { echo "lock tables lostnfound
write;" ; sed 's,#,,;s,.*,insert into lostnfound values(&);,'; } |
mysql ext2backup
livecd ~ # { echo "select inode, curdir, name from lostnfound inner
join entries using (inode) where name != '.' and name != '..';" |
mysql ext2backup | while read inode curdir name; do
base="/mnt/slave/smth-found"; echo "mkdir -p "$base/#$curdir"; mv
"/mnt/slave/lost+found/#${inode}" "$base/#$curdir/$name""; done; }
| sed 1d > renamer.sh
livecd ~ # { echo "select lostnfound.inode as lostinode,
if(parent.name <=> null, concat('#', entries.inode), parent.name) from
lostnfound inner join entries on (entries.name = '..' and curdir =
lostnfound.inode) left join entries as parent on (parent.name != '.'
and parent.name != '..' and parent.inode = entries.inode);" | mysql
ext2backup | while read inode curdir; do base="/mnt/slave/smth-found";
echo "mkdir -p "$base/$curdir"; mv
"/mnt/slave/lost+found/#${inode}" "$base/$curdir/#$inode""; done;
} | sed 1d > renamer.sh
После чего почти все в почти читаемом виде с ФС почти восстановлено (в
lost+found у меня осталось после данной махинации процентов 15% от
первоначального объема).
debugfs-ls-scan.pl
#!/usr/bin/perl
use DBI;
use strict;
my $db = DBI->connect("dbi:mysql:database=ext2backup", "root", "")
or die $DBI::errstr;
my $inserter = $db->prepare(
"INSERT INTO `entries`(`inode`,`curdir`,`name`, `size`) VALUES (?,?,?,?)");
my $counter = 0;
my $curdir = undef;
while (<>) {
my @chunks = split / /;
foreach my $chunk (@chunks) {
$chunk =~ s/ +$//;
if ($chunk =~ /^ ?([0-9]+) (([0-9]+)) (.*)$/) {
my ($inode, $size, $name) = ($1, $2, $3);
if ($name eq ".") {
$curdir = $inode;
}
# print "($inode, $size, $name) at $curdirn";
$inserter->execute($inode, $curdir, $name, $size)
or die $DBI::errstr;
}
else {
die "Invalid chunk: $chunk"
}
}
$counter++;
if (($counter % 100) == 0) {
print "$counter lines...n";
}
}