Detecção automática de parâmetros de RAID sem metadados e montagem integrada
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5d45273e0d
commit
b8dfe21fd7
9 files changed
+503
-1
No files matched your search
@@ -31,8 +31,22 @@ pub struct SavedKey {
|
|||||||
pub key_hex: String,
|
pub key_hex: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// RAID sem metadados: o que o usuário sabe; o resto é descoberto.
|
||||||
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
|
pub struct RaidSpec {
|
||||||
|
pub level: Option<crate::raid::Level>,
|
||||||
|
/// Tamanho da faixa em KiB.
|
||||||
|
pub chunk_kb: Option<u64>,
|
||||||
|
pub layout: Option<crate::raid::Layout>,
|
||||||
|
/// Ordem dos discos: índice 0 é o alvo principal, depois `members` na ordem dada.
|
||||||
|
pub order: Option<Vec<usize>>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
pub struct Assembly {
|
pub struct Assembly {
|
||||||
|
/// Trata o alvo e `members` como discos de um RAID sem metadados (parâmetros descobertos).
|
||||||
|
#[serde(default)]
|
||||||
|
pub raid: Option<RaidSpec>,
|
||||||
/// Outros discos ou imagens do mesmo conjunto (RAID/LVM), além do alvo principal.
|
/// Outros discos ou imagens do mesmo conjunto (RAID/LVM), além do alvo principal.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub members: Vec<String>,
|
pub members: Vec<String>,
|
||||||
@@ -48,7 +62,7 @@ pub struct Assembly {
|
|||||||
|
|
||||||
impl Assembly {
|
impl Assembly {
|
||||||
pub fn is_plain(&self) -> bool {
|
pub fn is_plain(&self) -> bool {
|
||||||
self.members.is_empty() && self.secrets.is_empty() && self.keys.is_empty()
|
self.members.is_empty() && self.secrets.is_empty() && self.keys.is_empty() && self.raid.is_none()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +91,8 @@ pub struct Assembled {
|
|||||||
pub extra_parts: Vec<PartInfo>,
|
pub extra_parts: Vec<PartInfo>,
|
||||||
/// Partições reais que são só contêineres (membros de RAID/LVM já remontados).
|
/// Partições reais que são só contêineres (membros de RAID/LVM já remontados).
|
||||||
pub covered: Vec<u64>,
|
pub covered: Vec<u64>,
|
||||||
|
/// O disco principal é só um membro de RAID: não varrer o disco cru.
|
||||||
|
pub skip_primary: bool,
|
||||||
pub notes: Vec<String>,
|
pub notes: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +184,7 @@ pub fn assemble_source(base: Arc<dyn Source>, asm: &Assembly) -> Result<Assemble
|
|||||||
keys: vec![],
|
keys: vec![],
|
||||||
extra_parts: vec![],
|
extra_parts: vec![],
|
||||||
covered: vec![],
|
covered: vec![],
|
||||||
|
skip_primary: false,
|
||||||
notes: vec![],
|
notes: vec![],
|
||||||
};
|
};
|
||||||
let mut members: Vec<Arc<dyn Source>> = vec![];
|
let mut members: Vec<Arc<dyn Source>> = vec![];
|
||||||
@@ -175,6 +192,16 @@ pub fn assemble_source(base: Arc<dyn Source>, asm: &Assembly) -> Result<Assemble
|
|||||||
members.push(open_path(Path::new(m)).map_err(|e| Error::msg(format!("{m}: {e}")))?);
|
members.push(open_path(Path::new(m)).map_err(|e| Error::msg(format!("{m}: {e}")))?);
|
||||||
}
|
}
|
||||||
let mut next_virtual = base.size().div_ceil(MIB) * MIB + MIB;
|
let mut next_virtual = base.size().div_ceil(MIB) * MIB + MIB;
|
||||||
|
if let Some(spec) = &asm.raid {
|
||||||
|
let mut disks: Vec<Arc<dyn Source>> = vec![base.clone()];
|
||||||
|
disks.extend(members.iter().cloned());
|
||||||
|
let params = pick_raid(&disks, spec, &mut out.notes)?;
|
||||||
|
let src: Arc<dyn Source> = Arc::new(crate::raid::RaidSource::new(params).map_err(Error::msg)?);
|
||||||
|
out.notes.push(format!("RAID montado a partir de {} disco(s) sem metadados.", disks.len()));
|
||||||
|
out.skip_primary = true;
|
||||||
|
members.clear();
|
||||||
|
add_virtual(&mut regions, &mut next_virtual, &mut out, src, "RAID".into());
|
||||||
|
}
|
||||||
let mut done_containers: std::collections::BTreeSet<String> = Default::default();
|
let mut done_containers: std::collections::BTreeSet<String> = Default::default();
|
||||||
let mut tried_pool = false;
|
let mut tried_pool = false;
|
||||||
// Rodadas: o que foi aberto/montado pode conter outro volume cifrado, RAID ou LVM.
|
// Rodadas: o que foi aberto/montado pode conter outro volume cifrado, RAID ou LVM.
|
||||||
@@ -368,3 +395,33 @@ fn add_virtual(regions: &mut Vec<Region>, next: &mut u64, out: &mut Assembled, s
|
|||||||
}
|
}
|
||||||
regions.push(Region { start, len, src, label });
|
regions.push(Region { start, len, src, label });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Escolhe os parâmetros do RAID: o que o usuário deu manda; o resto vem da detecção.
|
||||||
|
fn pick_raid(disks: &[Arc<dyn Source>], spec: &RaidSpec, notes: &mut Vec<String>) -> Result<crate::raid::RaidParams> {
|
||||||
|
use crate::raid::{Member, RaidParams};
|
||||||
|
if let (Some(level), Some(chunk), Some(layout), Some(order)) = (spec.level, spec.chunk_kb, spec.layout, spec.order.clone()) {
|
||||||
|
if order.len() == disks.len() && order.iter().all(|&i| i < disks.len()) {
|
||||||
|
let members = order.iter().map(|&i| Some(Member { src: disks[i].clone(), data_offset: 0 })).collect();
|
||||||
|
return Ok(RaidParams::simple(level, chunk * 1024, layout, members));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let cands = crate::raid_detect::detect(disks, &|| false);
|
||||||
|
let c = cands
|
||||||
|
.into_iter()
|
||||||
|
.find(|c| {
|
||||||
|
spec.level.map_or(true, |l| c.level == l)
|
||||||
|
&& spec.chunk_kb.map_or(true, |k| c.chunk == k * 1024)
|
||||||
|
&& spec.layout.map_or(true, |l| c.layout == l)
|
||||||
|
&& spec.order.as_ref().map_or(true, |o| *o == c.order)
|
||||||
|
})
|
||||||
|
.ok_or_else(|| Error::msg("não foi possível descobrir os parâmetros do RAID (nenhuma combinação produziu um sistema de arquivos coerente)"))?;
|
||||||
|
notes.push(format!(
|
||||||
|
"Parâmetros do RAID descobertos: {}, faixa de {} KiB, layout {:?}, ordem dos discos {:?} (pontuação {:.1}).",
|
||||||
|
c.level.name(),
|
||||||
|
c.chunk / 1024,
|
||||||
|
c.layout,
|
||||||
|
c.order,
|
||||||
|
c.score
|
||||||
|
));
|
||||||
|
Ok(c.params(disks))
|
||||||
|
}
|
||||||
@@ -831,3 +831,60 @@ fn remnant_extents(ext: &Ext, block: &[u8; 60]) -> ExtentList {
|
|||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Quão coerente é a estrutura do volume: fração das entradas de diretório cujo inode confere
|
||||||
|
/// com o tipo declarado. Serve para escolher parâmetros de RAID sem metadados.
|
||||||
|
pub fn consistency_score(src: &dyn Source, base: u64) -> Option<f64> {
|
||||||
|
let ext = Ext::open(src, base)?;
|
||||||
|
let mut queue = vec![2u64];
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
let (mut ok, mut total) = (0u32, 0u32);
|
||||||
|
let (mut dots_ok, mut dots_total) = (0u32, 0u32);
|
||||||
|
while let Some(d) = queue.pop() {
|
||||||
|
if !seen.insert(d) || total >= 600 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(ino) = ext.read_inode(d).filter(|i| i.is_dir()) else { continue };
|
||||||
|
let mut ents = vec![];
|
||||||
|
for e in ext.file_extents(&ino, ino.size.min(1 << 20)) {
|
||||||
|
if e.sparse {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut o = 0;
|
||||||
|
while o < e.len {
|
||||||
|
if let Ok(b) = read_vec(ext.src, e.offset + o, ext.bs as usize) {
|
||||||
|
if b.len() as u64 == ext.bs {
|
||||||
|
parse_dir_block(&b, ext.inodes, &mut ents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o += ext.bs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A entrada "." de um diretório aponta para ele mesmo: só confere se inode e bloco
|
||||||
|
// vieram do lugar certo.
|
||||||
|
dots_total += 1;
|
||||||
|
if ents.iter().any(|e| e.live && e.name == "." && e.ino == d) {
|
||||||
|
dots_ok += 1;
|
||||||
|
}
|
||||||
|
for en in ents.iter().filter(|e| e.live && e.name != "." && e.name != "..") {
|
||||||
|
total += 1;
|
||||||
|
if let Some(i) = ext.read_inode(en.ino) {
|
||||||
|
if i.mode != 0 && i.links > 0 {
|
||||||
|
ok += 1;
|
||||||
|
if i.is_dir() {
|
||||||
|
queue.push(en.ino);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if total >= 600 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if total == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let a = ok as f64 / total as f64;
|
||||||
|
let b = if dots_total > 0 { dots_ok as f64 / dots_total as f64 } else { 0.0 };
|
||||||
|
Some(1.0 + 5.0 * a + 5.0 * b)
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ pub mod owner;
|
|||||||
pub mod partition;
|
pub mod partition;
|
||||||
pub mod reconnect;
|
pub mod reconnect;
|
||||||
pub mod raid;
|
pub mod raid;
|
||||||
|
pub mod raid_detect;
|
||||||
pub mod recover;
|
pub mod recover;
|
||||||
pub mod report;
|
pub mod report;
|
||||||
pub mod scan;
|
pub mod scan;
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
//! Descobre sozinho nível, ordem dos discos, tamanho de faixa e layout de um RAID sem
|
||||||
|
//! metadados, testando as combinações e medindo a coerência do sistema de arquivos que sai.
|
||||||
|
|
||||||
|
use crate::partition;
|
||||||
|
use crate::raid::{Layout, Level, Member, RaidParams, RaidSource};
|
||||||
|
use crate::source::{read_vec, MemSource, Source};
|
||||||
|
use crate::types::FsKind;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Candidate {
|
||||||
|
pub level: Level,
|
||||||
|
pub chunk: u64,
|
||||||
|
pub layout: Layout,
|
||||||
|
/// Índices dos membros fornecidos, na ordem do array.
|
||||||
|
pub order: Vec<usize>,
|
||||||
|
pub score: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Candidate {
|
||||||
|
pub fn params(&self, members: &[Arc<dyn Source>]) -> RaidParams {
|
||||||
|
RaidParams::simple(
|
||||||
|
self.level,
|
||||||
|
self.chunk,
|
||||||
|
self.layout,
|
||||||
|
self.order.iter().map(|&i| Some(Member { src: members[i].clone(), data_offset: 0 })).collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PREFIX: u64 = 24 << 20;
|
||||||
|
const CHUNKS: [u64; 9] = [4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576];
|
||||||
|
|
||||||
|
fn permutations(n: usize) -> Vec<Vec<usize>> {
|
||||||
|
fn rec(cur: &mut Vec<usize>, used: &mut Vec<bool>, n: usize, out: &mut Vec<Vec<usize>>) {
|
||||||
|
if cur.len() == n {
|
||||||
|
out.push(cur.clone());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for i in 0..n {
|
||||||
|
if !used[i] {
|
||||||
|
used[i] = true;
|
||||||
|
cur.push(i);
|
||||||
|
rec(cur, used, n, out);
|
||||||
|
cur.pop();
|
||||||
|
used[i] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut out = vec![];
|
||||||
|
rec(&mut vec![], &mut vec![false; n], n, &mut out);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ntfs_score(src: &dyn Source, start: u64) -> f64 {
|
||||||
|
let Ok(b) = read_vec(src, start, 512) else { return 0.0 };
|
||||||
|
if b.len() < 512 || &b[3..11] != b"NTFS " {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
let bps = u16::from_le_bytes([b[11], b[12]]) as u64;
|
||||||
|
let spc = b[13] as u64;
|
||||||
|
let mft = u64::from_le_bytes(b[0x30..0x38].try_into().unwrap());
|
||||||
|
let rs = b[0x40] as i8;
|
||||||
|
let rec = if rs > 0 { rs as u64 * bps * spc } else { 1u64 << (-rs) };
|
||||||
|
if bps == 0 || spc == 0 || rec == 0 || rec > 65536 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
let mft_off = start + mft * bps * spc;
|
||||||
|
let (mut ok, mut total) = (0, 0);
|
||||||
|
for k in [0u64, 1, 2, 3, 5, 11, 16, 24, 40, 100, 250, 600, 1500, 3000, 6000, 12000, 24000, 50000] {
|
||||||
|
let off = mft_off + k * rec;
|
||||||
|
if off + rec > src.size() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let Ok(r) = read_vec(src, off, 64) else { continue };
|
||||||
|
if r.len() < 64 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Registros além do fim do MFT estão zerados: não contam contra.
|
||||||
|
if r.iter().all(|&x| x == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
total += 1;
|
||||||
|
if &r[..4] == b"FILE" && u32::from_le_bytes(r[0x2C..0x30].try_into().unwrap()) as u64 == k {
|
||||||
|
ok += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if total == 0 {
|
||||||
|
1.0
|
||||||
|
} else {
|
||||||
|
1.0 + 10.0 * ok as f64 / total as f64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Coerência do volume que começa em `start`.
|
||||||
|
fn volume_score(src: &dyn Source, start: u64, fs: FsKind) -> f64 {
|
||||||
|
match fs {
|
||||||
|
FsKind::Ntfs => ntfs_score(src, start),
|
||||||
|
FsKind::Ext => crate::fs::ext::consistency_score(src, start).unwrap_or(0.5),
|
||||||
|
FsKind::Unknown => 0.0,
|
||||||
|
_ => 1.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fs_score(src: &dyn Source) -> f64 {
|
||||||
|
let layout = partition::analyze(src, false);
|
||||||
|
let mut best = 0.0f64;
|
||||||
|
for p in &layout.partitions {
|
||||||
|
best = best.max(volume_score(src, p.start, p.fs) + if p.fs != FsKind::Unknown { 0.5 } else { 0.0 });
|
||||||
|
}
|
||||||
|
best
|
||||||
|
}
|
||||||
|
|
||||||
|
fn first_sector_plausible(src: &dyn Source) -> bool {
|
||||||
|
let Ok(b) = read_vec(src, 0, 4096) else { return false };
|
||||||
|
b.len() >= 1100 && ((b[510] == 0x55 && b[511] == 0xAA) || partition::detect_fs_in(&b).is_some())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Testa as combinações e devolve as melhores (maior pontuação primeiro).
|
||||||
|
pub fn detect(members: &[Arc<dyn Source>], cancelled: &(dyn Fn() -> bool + Sync)) -> Vec<Candidate> {
|
||||||
|
let n = members.len();
|
||||||
|
if n < 2 {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
// Trabalha sobre um prefixo em memória de cada disco.
|
||||||
|
let cached: Vec<Arc<dyn Source>> = members
|
||||||
|
.iter()
|
||||||
|
.map(|m| {
|
||||||
|
let len = m.size().min(PREFIX) as usize;
|
||||||
|
Arc::new(MemSource(read_vec(m.as_ref(), 0, len).unwrap_or_default())) as Arc<dyn Source>
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let min_len = cached.iter().map(|c| c.size()).min().unwrap_or(0);
|
||||||
|
if min_len < 1 << 20 {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let mut out: Vec<Candidate> = vec![];
|
||||||
|
|
||||||
|
// RAID 1: cópias idênticas.
|
||||||
|
let probe = |s: &Arc<dyn Source>| read_vec(s.as_ref(), 0, 1 << 20).unwrap_or_default();
|
||||||
|
let first = probe(&cached[0]);
|
||||||
|
if first.iter().any(|&x| x != 0) && cached.iter().skip(1).all(|c| probe(c) == first) {
|
||||||
|
let c = Candidate { level: Level::R1, chunk: 65536, layout: Layout::LeftSymmetric, order: (0..n).collect(), score: fs_score(cached[0].as_ref()) + 1.0 };
|
||||||
|
out.push(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
// RAID 5/4 satisfaz "XOR de todos os discos = 0" em toda faixa.
|
||||||
|
let sample = 4usize << 20;
|
||||||
|
let bufs: Vec<Vec<u8>> = cached.iter().map(|c| read_vec(c.as_ref(), 0, sample).unwrap_or_default()).collect();
|
||||||
|
let mut xor_zero = 0usize;
|
||||||
|
let mut nonzero_rows = 0usize;
|
||||||
|
for blk in 0..sample / 4096 {
|
||||||
|
let mut any = false;
|
||||||
|
let mut z = true;
|
||||||
|
for i in 0..4096 {
|
||||||
|
let mut x = 0u8;
|
||||||
|
for b in &bufs {
|
||||||
|
x ^= b[blk * 4096 + i];
|
||||||
|
any |= b[blk * 4096 + i] != 0;
|
||||||
|
}
|
||||||
|
if x != 0 {
|
||||||
|
z = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if any {
|
||||||
|
nonzero_rows += 1;
|
||||||
|
if z {
|
||||||
|
xor_zero += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let parity5 = nonzero_rows > 8 && xor_zero * 100 >= nonzero_rows * 95;
|
||||||
|
|
||||||
|
let mut levels: Vec<Level> = vec![];
|
||||||
|
if parity5 && n >= 3 {
|
||||||
|
levels.push(Level::R5);
|
||||||
|
} else {
|
||||||
|
levels.push(Level::R0);
|
||||||
|
if n >= 4 {
|
||||||
|
levels.push(Level::R6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let ok_first: Vec<bool> = cached.iter().map(|c| first_sector_plausible(c.as_ref())).collect();
|
||||||
|
let any_first = ok_first.iter().any(|&b| b);
|
||||||
|
let perms = permutations(n);
|
||||||
|
for level in levels {
|
||||||
|
let layouts: Vec<Layout> = if matches!(level, Level::R0) { vec![Layout::LeftSymmetric] } else { Layout::all_raid5().to_vec() };
|
||||||
|
for &chunk in &CHUNKS {
|
||||||
|
for layout in &layouts {
|
||||||
|
for order in &perms {
|
||||||
|
if cancelled() {
|
||||||
|
return finish(out);
|
||||||
|
}
|
||||||
|
let cand = Candidate { level, chunk, layout: *layout, order: order.clone(), score: 0.0 };
|
||||||
|
let params = cand.params(&cached);
|
||||||
|
let Ok(r) = RaidSource::new(params) else { continue };
|
||||||
|
// O primeiro setor do volume precisa estar num disco que o tenha.
|
||||||
|
if any_first {
|
||||||
|
let mut b = [0u8; 512];
|
||||||
|
let _ = r.read_at(0, &mut b);
|
||||||
|
let m0 = order.iter().position(|&i| {
|
||||||
|
let mut c = [0u8; 512];
|
||||||
|
let _ = cached[i].read_at(0, &mut c);
|
||||||
|
c == b
|
||||||
|
});
|
||||||
|
match m0 {
|
||||||
|
Some(p) if ok_first[order[p]] => {}
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let score = fs_score(&r);
|
||||||
|
if score > 0.0 {
|
||||||
|
out.push(Candidate { score, ..cand });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finish(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(mut out: Vec<Candidate>) -> Vec<Candidate> {
|
||||||
|
out.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
|
out.truncate(10);
|
||||||
|
out
|
||||||
|
}
|
||||||
@@ -84,6 +84,11 @@ pub fn run(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut layout = layout;
|
let mut layout = layout;
|
||||||
|
if opts.skip_primary {
|
||||||
|
layout.partitions.clear();
|
||||||
|
layout.gaps.clear();
|
||||||
|
layout.table = "membro de RAID".into();
|
||||||
|
}
|
||||||
// Volumes lógicos e outros volumes montados que não estão na tabela do disco.
|
// Volumes lógicos e outros volumes montados que não estão na tabela do disco.
|
||||||
for p in &opts.extra_parts {
|
for p in &opts.extra_parts {
|
||||||
if !layout.partitions.iter().any(|q| q.start == p.start) {
|
if !layout.partitions.iter().any(|q| q.start == p.start) {
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ impl Engine {
|
|||||||
let asm = crate::assemble::assemble_source(raw, &opts.assembly)?;
|
let asm = crate::assemble::assemble_source(raw, &opts.assembly)?;
|
||||||
opts.extra_parts = asm.extra_parts.clone();
|
opts.extra_parts = asm.extra_parts.clone();
|
||||||
opts.covered = asm.covered.clone();
|
opts.covered = asm.covered.clone();
|
||||||
|
opts.skip_primary = asm.skip_primary;
|
||||||
if opts.assembly.remember_keys {
|
if opts.assembly.remember_keys {
|
||||||
opts.assembly.keys = asm.keys.clone();
|
opts.assembly.keys = asm.keys.clone();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -327,6 +327,9 @@ pub struct ScanOptions {
|
|||||||
/// Partições que só contêm RAID/LVM remontado: a varredura entra pelos volumes virtuais.
|
/// Partições que só contêm RAID/LVM remontado: a varredura entra pelos volumes virtuais.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub covered: Vec<u64>,
|
pub covered: Vec<u64>,
|
||||||
|
/// O disco principal é só membro de RAID sem metadados: varre apenas os volumes virtuais.
|
||||||
|
#[serde(default)]
|
||||||
|
pub skip_primary: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_max_carve() -> u64 {
|
fn default_max_carve() -> u64 {
|
||||||
@@ -343,6 +346,7 @@ impl Default for ScanOptions {
|
|||||||
assembly: Default::default(),
|
assembly: Default::default(),
|
||||||
extra_parts: vec![],
|
extra_parts: vec![],
|
||||||
covered: vec![],
|
covered: vec![],
|
||||||
|
skip_primary: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,3 +121,69 @@ fn deleted_file_on_degraded_raid5() {
|
|||||||
let p = if Path::new(p).is_absolute() { Path::new(p).to_path_buf() } else { dest.path().join(p) };
|
let p = if Path::new(p).is_absolute() { Path::new(p).to_path_buf() } else { dest.path().join(p) };
|
||||||
assert!(std::fs::read(p).unwrap() == doc);
|
assert!(std::fs::read(p).unwrap() == doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn raid5_without_metadata_is_detected_and_scanned() {
|
||||||
|
if !(have("mke2fs") && have("debugfs")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
init();
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let root = dir.path().join("root");
|
||||||
|
for d in 0..10 {
|
||||||
|
std::fs::create_dir_all(root.join(format!("d{d}"))).unwrap();
|
||||||
|
for f in 0..30 {
|
||||||
|
std::fs::write(root.join(format!("d{d}/f{f}.txt")), vec![b'x'; 500 + f * 50]).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let doc = data(9, 200_000);
|
||||||
|
std::fs::write(root.join("d3/perdido.dat"), &doc).unwrap();
|
||||||
|
let plain = dir.path().join("plain.img");
|
||||||
|
let (n, chunk) = (3usize, 64 * 1024usize);
|
||||||
|
let dd = n - 1;
|
||||||
|
let rows = 160usize;
|
||||||
|
std::fs::File::create(&plain).unwrap().set_len((rows * dd * chunk) as u64).unwrap();
|
||||||
|
assert!(Command::new("mke2fs").args(["-q", "-F", "-t", "ext4", "-d"]).arg(&root).arg(&plain).status().unwrap().success());
|
||||||
|
Command::new("debugfs").arg("-w").arg("-R").arg("rm /d3/perdido.dat").arg(&plain).output().unwrap();
|
||||||
|
let logical = std::fs::read(&plain).unwrap();
|
||||||
|
// left-asymmetric
|
||||||
|
let mut disks = vec![vec![0u8; rows * chunk]; n];
|
||||||
|
for row in 0..rows {
|
||||||
|
let pd = n - 1 - row % n;
|
||||||
|
let mut p = vec![0u8; chunk];
|
||||||
|
for (k, disk) in (0..n).filter(|&i| i != pd).enumerate() {
|
||||||
|
let s = &logical[(row * dd + k) * chunk..(row * dd + k + 1) * chunk];
|
||||||
|
disks[disk][row * chunk..(row + 1) * chunk].copy_from_slice(s);
|
||||||
|
for i in 0..chunk {
|
||||||
|
p[i] ^= s[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disks[pd][row * chunk..(row + 1) * chunk].copy_from_slice(&p);
|
||||||
|
}
|
||||||
|
let order = [1usize, 2, 0]; // fornecidos fora de ordem
|
||||||
|
let mut paths = vec![];
|
||||||
|
for (j, &i) in order.iter().enumerate() {
|
||||||
|
let p = dir.path().join(format!("m{j}.img"));
|
||||||
|
std::fs::write(&p, &disks[i]).unwrap();
|
||||||
|
paths.push(p);
|
||||||
|
}
|
||||||
|
let base: Arc<dyn Source> = Arc::new(FileSource::open(&paths[0]).unwrap());
|
||||||
|
let asm = Assembly {
|
||||||
|
members: paths[1..].iter().map(|p| p.to_string_lossy().to_string()).collect(),
|
||||||
|
raid: Some(guara_core::assemble::RaidSpec::default()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let a = assemble_source(base, &asm).unwrap();
|
||||||
|
assert!(a.skip_primary);
|
||||||
|
assert!(a.notes.iter().any(|n| n.contains("descobertos")), "{:?}", a.notes);
|
||||||
|
let opts = ScanOptions { mode: ScanMode::Deep, extra_parts: a.extra_parts.clone(), skip_primary: true, ..Default::default() };
|
||||||
|
let mut sink = Collect::default();
|
||||||
|
let out = scan::run(a.source.as_ref(), &opts, None, &[], &|| false, &mut sink);
|
||||||
|
assert!(matches!(out, Outcome::Completed));
|
||||||
|
let f = sink.found.iter().find(|f| f.name == "perdido.dat").expect("perdido.dat");
|
||||||
|
let dest = tempfile::tempdir().unwrap();
|
||||||
|
let rep = recover::recover(a.source.as_ref(), &[f], dest.path(), &|| false, &mut |_, _, _| {}).unwrap();
|
||||||
|
let p = &rep.recovered[0].path;
|
||||||
|
let p = if Path::new(p).is_absolute() { Path::new(p).to_path_buf() } else { dest.path().join(p) };
|
||||||
|
assert!(std::fs::read(p).unwrap() == doc);
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
//! Descoberta automática dos parâmetros de um RAID sem metadados.
|
||||||
|
|
||||||
|
use guara_core::raid::Level;
|
||||||
|
use guara_core::raid_detect;
|
||||||
|
use guara_core::source::{MemSource, Source};
|
||||||
|
use std::process::Command;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
fn have(t: &str) -> bool {
|
||||||
|
Command::new("sh").args(["-c", &format!("command -v {t}")]).output().map(|o| o.status.success()).unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ext4_image(mib: u64) -> Option<Vec<u8>> {
|
||||||
|
if !have("mke2fs") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let root = dir.path().join("root");
|
||||||
|
for d in 0..12 {
|
||||||
|
std::fs::create_dir_all(root.join(format!("pasta{d}/sub"))).unwrap();
|
||||||
|
for f in 0..40 {
|
||||||
|
std::fs::write(root.join(format!("pasta{d}/arq{f}.txt")), vec![b'a' + (f % 26) as u8; 300 + f * 37]).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let img = dir.path().join("e.img");
|
||||||
|
std::fs::File::create(&img).unwrap().set_len(mib << 20).unwrap();
|
||||||
|
assert!(Command::new("mke2fs").args(["-q", "-F", "-t", "ext4", "-b", "1024", "-d"]).arg(&root).arg(&img).status().unwrap().success());
|
||||||
|
Some(std::fs::read(img).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn finds_raid5_chunk_layout_and_shuffled_order() {
|
||||||
|
let Some(logical) = ext4_image(24) else { return };
|
||||||
|
let (n, chunk) = (3usize, 32768usize);
|
||||||
|
let dd = n - 1;
|
||||||
|
let rows = logical.len() / (chunk * dd);
|
||||||
|
let logical = &logical[..rows * chunk * dd];
|
||||||
|
// right-asymmetric
|
||||||
|
let mut disks = vec![vec![0u8; rows * chunk]; n];
|
||||||
|
for row in 0..rows {
|
||||||
|
let pd = row % n;
|
||||||
|
let order: Vec<usize> = (0..n).filter(|&i| i != pd).collect();
|
||||||
|
let mut p = vec![0u8; chunk];
|
||||||
|
for (k, &disk) in order.iter().enumerate() {
|
||||||
|
let s = &logical[(row * dd + k) * chunk..(row * dd + k + 1) * chunk];
|
||||||
|
disks[disk][row * chunk..(row + 1) * chunk].copy_from_slice(s);
|
||||||
|
for i in 0..chunk {
|
||||||
|
p[i] ^= s[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disks[pd][row * chunk..(row + 1) * chunk].copy_from_slice(&p);
|
||||||
|
}
|
||||||
|
// Entrega os discos embaralhados: fornecidos = [2, 0, 1] do array real.
|
||||||
|
let given: Vec<Arc<dyn Source>> = [2usize, 0, 1].iter().map(|&i| Arc::new(MemSource(disks[i].clone())) as Arc<dyn Source>).collect();
|
||||||
|
let found = raid_detect::detect(&given, &|| false);
|
||||||
|
assert!(!found.is_empty());
|
||||||
|
for c in found.iter().take(6) {
|
||||||
|
eprintln!("CAND {:?} {} {:?} {:?} {:.2}", c.level, c.chunk, c.layout, c.order, c.score);
|
||||||
|
}
|
||||||
|
let best = &found[0];
|
||||||
|
assert_eq!(best.level, Level::R5);
|
||||||
|
assert_eq!(best.chunk, chunk as u64);
|
||||||
|
// Layouts e ordens diferentes podem ser equivalentes; o que importa é o conteúdo montado.
|
||||||
|
let r = guara_core::raid::RaidSource::new(best.params(&given)).unwrap();
|
||||||
|
let got = guara_core::source::read_vec(&r, 0, logical.len()).unwrap();
|
||||||
|
assert!(got == logical, "montagem escolhida ({:?}, {:?}, {:?}) não reproduz o volume", best.layout, best.order, best.level);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn finds_raid0() {
|
||||||
|
let Some(logical) = ext4_image(24) else { return };
|
||||||
|
let (n, chunk) = (2usize, 65536usize);
|
||||||
|
let rows = logical.len() / (chunk * n);
|
||||||
|
let mut disks = vec![vec![]; n];
|
||||||
|
for c in 0..rows * n {
|
||||||
|
disks[c % n].extend_from_slice(&logical[c * chunk..(c + 1) * chunk]);
|
||||||
|
}
|
||||||
|
let given: Vec<Arc<dyn Source>> = [1usize, 0].iter().map(|&i| Arc::new(MemSource(disks[i].clone())) as Arc<dyn Source>).collect();
|
||||||
|
let found = raid_detect::detect(&given, &|| false);
|
||||||
|
let best = &found[0];
|
||||||
|
assert_eq!(best.level, Level::R0);
|
||||||
|
assert_eq!(best.chunk, chunk as u64);
|
||||||
|
assert_eq!(best.order, vec![1, 0]);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user