RAID (0/1/4/5/6/10/linear, mdadm 0.90 e 1.x, degradado) e LVM2 remontados no disco virtual
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c014526761
commit
5d45273e0d
11 files changed
+1777
-6
No files matched your search
@@ -75,6 +75,8 @@ pub struct Assembled {
|
|||||||
pub keys: Vec<SavedKey>,
|
pub keys: Vec<SavedKey>,
|
||||||
/// Volumes lógicos / partições extras que não estão na tabela do disco.
|
/// Volumes lógicos / partições extras que não estão na tabela do disco.
|
||||||
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).
|
||||||
|
pub covered: Vec<u64>,
|
||||||
pub notes: Vec<String>,
|
pub notes: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +121,43 @@ pub fn assemble(path: &str, asm: &Assembly) -> Result<Assembled> {
|
|||||||
assemble_source(base, asm)
|
assemble_source(base, asm)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MIB: u64 = 1 << 20;
|
||||||
|
|
||||||
|
/// Coloca `src` em `start`; se cair dentro de uma região virtual, a substitui por dentro.
|
||||||
|
fn patch(regions: &mut Vec<Region>, start: u64, src: Arc<dyn Source>, label: String) {
|
||||||
|
let len = src.size();
|
||||||
|
if let Some(r) = regions.iter_mut().find(|r| r.start <= start && start < r.start + r.len) {
|
||||||
|
let inner = Region { start: start - r.start, len, src, label: label.clone() };
|
||||||
|
let wrapped = Overlay::new(r.src.clone(), vec![inner]);
|
||||||
|
if start == r.start {
|
||||||
|
r.len = r.len.max(len);
|
||||||
|
}
|
||||||
|
r.src = Arc::new(wrapped);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
regions.push(Region { start, len, src, label });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Candidato a membro de RAID/LVM: uma janela de disco (inteiro ou partição).
|
||||||
|
struct Window {
|
||||||
|
src: Arc<dyn Source>,
|
||||||
|
/// Início no espaço do disco virtual, se a janela faz parte dele.
|
||||||
|
primary_start: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn windows_of(src: &Arc<dyn Source>, primary: bool) -> Vec<Window> {
|
||||||
|
let mut v = vec![Window { src: src.clone(), primary_start: primary.then_some(0) }];
|
||||||
|
let layout = partition::analyze(src.as_ref(), false);
|
||||||
|
for p in &layout.partitions {
|
||||||
|
let len = if p.len == 0 { src.size().saturating_sub(p.start) } else { p.len };
|
||||||
|
v.push(Window {
|
||||||
|
src: Arc::new(SubSource::new(src.clone(), p.start, len)),
|
||||||
|
primary_start: primary.then_some(p.start),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
pub fn assemble_source(base: Arc<dyn Source>, asm: &Assembly) -> Result<Assembled> {
|
pub fn assemble_source(base: Arc<dyn Source>, asm: &Assembly) -> Result<Assembled> {
|
||||||
let secrets = to_secrets(asm)?;
|
let secrets = to_secrets(asm)?;
|
||||||
let mut regions: Vec<Region> = vec![];
|
let mut regions: Vec<Region> = vec![];
|
||||||
@@ -128,16 +167,120 @@ pub fn assemble_source(base: Arc<dyn Source>, asm: &Assembly) -> Result<Assemble
|
|||||||
locked: vec![],
|
locked: vec![],
|
||||||
keys: vec![],
|
keys: vec![],
|
||||||
extra_parts: vec![],
|
extra_parts: vec![],
|
||||||
|
covered: vec![],
|
||||||
notes: vec![],
|
notes: vec![],
|
||||||
};
|
};
|
||||||
// Até três rodadas: o que foi aberto pode conter outro volume cifrado.
|
let mut members: Vec<Arc<dyn Source>> = vec![];
|
||||||
for _ in 0..3 {
|
for m in &asm.members {
|
||||||
|
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 done_containers: std::collections::BTreeSet<String> = Default::default();
|
||||||
|
let mut tried_pool = false;
|
||||||
|
// Rodadas: o que foi aberto/montado pode conter outro volume cifrado, RAID ou LVM.
|
||||||
|
for round in 0..4 {
|
||||||
let current: Arc<dyn Source> = Arc::new(Overlay::new(base.clone(), regions.clone()));
|
let current: Arc<dyn Source> = Arc::new(Overlay::new(base.clone(), regions.clone()));
|
||||||
let layout = partition::analyze(current.as_ref(), false);
|
let layout = partition::analyze(current.as_ref(), false);
|
||||||
|
let mut all_parts: Vec<PartInfo> = layout.partitions.clone();
|
||||||
|
for p in &out.extra_parts {
|
||||||
|
if !all_parts.iter().any(|q| q.start == p.start) {
|
||||||
|
all_parts.push(p.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut progress = false;
|
let mut progress = false;
|
||||||
|
|
||||||
|
// 1) RAID e LVM: reúne janelas do disco principal e dos membros extras.
|
||||||
|
if round == 0 || !tried_pool || !out.extra_parts.is_empty() {
|
||||||
|
let mut pool: Vec<Window> = vec![];
|
||||||
|
pool.push(Window { src: current.clone(), primary_start: Some(0) });
|
||||||
|
for p in &all_parts {
|
||||||
|
if matches!(p.fs, FsKind::Raid | FsKind::Lvm) {
|
||||||
|
let len = if p.len == 0 { current.size().saturating_sub(p.start) } else { p.len };
|
||||||
|
pool.push(Window { src: Arc::new(SubSource::new(current.clone(), p.start, len)), primary_start: Some(p.start) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for m in &members {
|
||||||
|
pool.extend(windows_of(m, false));
|
||||||
|
}
|
||||||
|
tried_pool = true;
|
||||||
|
// RAID
|
||||||
|
let mut md: Vec<(Arc<dyn Source>, crate::raid::MdSuper)> = vec![];
|
||||||
|
let mut md_primary: Vec<u64> = vec![];
|
||||||
|
for w in &pool {
|
||||||
|
if let Some(sup) = crate::raid::read_md_super(w.src.as_ref()) {
|
||||||
|
if let Some(st) = w.primary_start {
|
||||||
|
md_primary.push(st);
|
||||||
|
}
|
||||||
|
md.push((w.src.clone(), sup));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let new_md: Vec<_> = md.into_iter().filter(|(_, s)| !done_containers.contains(&format!("md{}", crypt::to_hex(&s.uuid)))).collect();
|
||||||
|
if !new_md.is_empty() {
|
||||||
|
for (_, s) in &new_md {
|
||||||
|
done_containers.insert(format!("md{}", crypt::to_hex(&s.uuid)));
|
||||||
|
}
|
||||||
|
for res in crate::raid::assemble_md(new_md) {
|
||||||
|
match res {
|
||||||
|
Ok(a) => {
|
||||||
|
let label = format!("{} {}", a.info.level.name(), if a.name.is_empty() { a.uuid.clone() } else { a.name.clone() });
|
||||||
|
if a.missing > 0 {
|
||||||
|
out.notes.push(format!("{label}: montado sem {} membro(s) (modo degradado).", a.missing));
|
||||||
|
} else {
|
||||||
|
out.notes.push(format!("{label} montado."));
|
||||||
|
}
|
||||||
|
add_virtual(&mut regions, &mut next_virtual, &mut out, a.source.clone(), label);
|
||||||
|
progress = true;
|
||||||
|
}
|
||||||
|
Err(e) => out.notes.push(format!("RAID não montado: {e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.covered.extend(md_primary);
|
||||||
|
}
|
||||||
|
// LVM
|
||||||
|
let mut pvs: Vec<(Arc<dyn Source>, crate::lvm::PvInfo)> = vec![];
|
||||||
|
let mut pv_primary: Vec<u64> = vec![];
|
||||||
|
let md_srcs: Vec<Arc<dyn Source>> = regions.iter().map(|r| r.src.clone()).collect();
|
||||||
|
let mut lvm_pool: Vec<(Arc<dyn Source>, Option<u64>)> = pool.iter().map(|w| (w.src.clone(), w.primary_start)).collect();
|
||||||
|
for (r, s) in regions.iter().zip(md_srcs) {
|
||||||
|
lvm_pool.push((s, Some(r.start)));
|
||||||
|
}
|
||||||
|
for (src, st) in lvm_pool {
|
||||||
|
if let Some(pv) = crate::lvm::read_pv(src.as_ref()) {
|
||||||
|
if !done_containers.insert(format!("pv{}", pv.uuid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(st) = st {
|
||||||
|
pv_primary.push(st);
|
||||||
|
}
|
||||||
|
pvs.push((src, pv));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !pvs.is_empty() {
|
||||||
|
for vg in crate::lvm::assemble_vgs(pvs) {
|
||||||
|
for w in &vg.warnings {
|
||||||
|
out.notes.push(format!("LVM {}: {w}", vg.name));
|
||||||
|
}
|
||||||
|
for (name, lv) in vg.lvs {
|
||||||
|
add_virtual(&mut regions, &mut next_virtual, &mut out, lv, format!("LVM {}/{}", vg.name, name));
|
||||||
|
progress = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.covered.extend(pv_primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Desbloqueio de volumes cifrados.
|
||||||
|
let current: Arc<dyn Source> = Arc::new(Overlay::new(base.clone(), regions.clone()));
|
||||||
|
let layout = partition::analyze(current.as_ref(), false);
|
||||||
|
let mut all_parts: Vec<PartInfo> = layout.partitions.clone();
|
||||||
|
for p in &out.extra_parts {
|
||||||
|
if !all_parts.iter().any(|q| q.start == p.start) {
|
||||||
|
all_parts.push(p.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
out.locked.clear();
|
out.locked.clear();
|
||||||
for p in &layout.partitions {
|
for p in &all_parts {
|
||||||
if p.encrypted.is_none() || regions.iter().any(|r| r.start == p.start) {
|
if p.encrypted.is_none() || out.unlocked.iter().any(|u| u.start == p.start) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let len = if p.len == 0 { current.size().saturating_sub(p.start) } else { p.len };
|
let len = if p.len == 0 { current.size().saturating_sub(p.start) } else { p.len };
|
||||||
@@ -160,7 +303,7 @@ pub fn assemble_source(base: Arc<dyn Source>, asm: &Assembly) -> Result<Assemble
|
|||||||
if let Some(k) = &u.volume_key {
|
if let Some(k) = &u.volume_key {
|
||||||
out.keys.push(SavedKey { start: p.start, kind: kind_name(p.fs).into(), key_hex: crypt::to_hex(k) });
|
out.keys.push(SavedKey { start: p.start, kind: kind_name(p.fs).into(), key_hex: crypt::to_hex(k) });
|
||||||
}
|
}
|
||||||
regions.push(Region { start: p.start, len: ulen, src: u.source, label: u.description });
|
patch(&mut regions, p.start, u.source, u.description);
|
||||||
progress = true;
|
progress = true;
|
||||||
}
|
}
|
||||||
Err(e) => out.locked.push(LockedInfo {
|
Err(e) => out.locked.push(LockedInfo {
|
||||||
@@ -179,6 +322,49 @@ pub fn assemble_source(base: Arc<dyn Source>, asm: &Assembly) -> Result<Assemble
|
|||||||
}
|
}
|
||||||
if !regions.is_empty() {
|
if !regions.is_empty() {
|
||||||
out.source = Arc::new(Overlay::new(base, regions));
|
out.source = Arc::new(Overlay::new(base, regions));
|
||||||
|
// Atualiza o que se sabe sobre as partições virtuais (algumas foram desbloqueadas depois).
|
||||||
|
for p in &mut out.extra_parts {
|
||||||
|
if let Some((fs, len)) = partition::detect_fs(out.source.as_ref(), p.start) {
|
||||||
|
p.fs = fs;
|
||||||
|
p.encrypted = fs.encrypted().map(String::from);
|
||||||
|
if let Some(l) = len {
|
||||||
|
p.len = p.len.min(l).max(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
out.covered.sort_unstable();
|
||||||
|
out.covered.dedup();
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Anexa um volume virtual depois do fim do disco e registra as partições que ele contém.
|
||||||
|
fn add_virtual(regions: &mut Vec<Region>, next: &mut u64, out: &mut Assembled, src: Arc<dyn Source>, label: String) {
|
||||||
|
let start = *next;
|
||||||
|
let len = src.size();
|
||||||
|
if len == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*next = (start + len).div_ceil(MIB) * MIB + MIB;
|
||||||
|
let inner = partition::analyze(src.as_ref(), false);
|
||||||
|
if inner.partitions.is_empty() {
|
||||||
|
out.extra_parts.push(PartInfo {
|
||||||
|
index: 0,
|
||||||
|
start,
|
||||||
|
len,
|
||||||
|
fs: FsKind::Unknown,
|
||||||
|
label: Some(label.clone()),
|
||||||
|
lost: false,
|
||||||
|
encrypted: None,
|
||||||
|
table_type: Some(label.clone()),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
for p in inner.partitions {
|
||||||
|
let mut q = p.clone();
|
||||||
|
q.start += start;
|
||||||
|
q.label = q.label.or_else(|| Some(label.clone()));
|
||||||
|
out.extra_parts.push(q);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
regions.push(Region { start, len, src, label });
|
||||||
|
}
|
||||||
@@ -16,11 +16,13 @@ pub mod hashing;
|
|||||||
pub mod health;
|
pub mod health;
|
||||||
pub mod http;
|
pub mod http;
|
||||||
pub mod image;
|
pub mod image;
|
||||||
|
pub mod lvm;
|
||||||
pub mod meta;
|
pub mod meta;
|
||||||
pub mod notify;
|
pub mod notify;
|
||||||
pub mod owner;
|
pub mod owner;
|
||||||
pub mod partition;
|
pub mod partition;
|
||||||
pub mod reconnect;
|
pub mod reconnect;
|
||||||
|
pub mod raid;
|
||||||
pub mod recover;
|
pub mod recover;
|
||||||
pub mod report;
|
pub mod report;
|
||||||
pub mod scan;
|
pub mod scan;
|
||||||
|
|||||||
@@ -0,0 +1,508 @@
|
|||||||
|
//! LVM2: lê o rótulo do volume físico (PV) e o texto de metadados, e remonta os volumes
|
||||||
|
//! lógicos (lineares, em faixas, espelhados e RAID 0/1/5/6/10 do LVM).
|
||||||
|
|
||||||
|
use crate::raid::{Layout, Level, Member, RaidParams, RaidSource};
|
||||||
|
use crate::source::{read_vec, Source};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::io;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
const MDA_MAGIC: &[u8; 16] = b" LVM2 x[5A%r0N*>";
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct PvInfo {
|
||||||
|
/// UUID de 32 caracteres, sem hífens.
|
||||||
|
pub uuid: String,
|
||||||
|
pub size: u64,
|
||||||
|
pub seqno: u64,
|
||||||
|
pub vg_name: String,
|
||||||
|
pub text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lê rótulo + metadados de um PV.
|
||||||
|
pub fn read_pv(src: &dyn Source) -> Option<PvInfo> {
|
||||||
|
let head = read_vec(src, 0, 2048).ok()?;
|
||||||
|
if head.len() < 2048 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let sector = (0..4).find(|&s| &head[s * 512..s * 512 + 8] == b"LABELONE")?;
|
||||||
|
let lab = &head[sector * 512..(sector + 1) * 512];
|
||||||
|
if &lab[24..32] != b"LVM2 001" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let off = u32::from_le_bytes(lab[20..24].try_into().ok()?) as usize;
|
||||||
|
let pv = lab.get(off..)?;
|
||||||
|
let uuid = String::from_utf8_lossy(pv.get(..32)?).to_string();
|
||||||
|
let size = u64::from_le_bytes(pv.get(32..40)?.try_into().ok()?);
|
||||||
|
// Lista de áreas de dados, depois de áreas de metadados; cada lista termina em (0,0).
|
||||||
|
let mut o = 40;
|
||||||
|
let rd = |o: usize| -> Option<(u64, u64)> {
|
||||||
|
Some((
|
||||||
|
u64::from_le_bytes(pv.get(o..o + 8)?.try_into().ok()?),
|
||||||
|
u64::from_le_bytes(pv.get(o + 8..o + 16)?.try_into().ok()?),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
loop {
|
||||||
|
let (a, b) = rd(o)?;
|
||||||
|
o += 16;
|
||||||
|
if a == 0 && b == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut mdas = vec![];
|
||||||
|
loop {
|
||||||
|
let (a, b) = rd(o)?;
|
||||||
|
o += 16;
|
||||||
|
if a == 0 && b == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
mdas.push((a, b));
|
||||||
|
}
|
||||||
|
let mut best: Option<PvInfo> = None;
|
||||||
|
for (mda_off, mda_size) in mdas {
|
||||||
|
let Ok(h) = read_vec(src, mda_off, 512) else { continue };
|
||||||
|
if h.len() < 512 || &h[4..20] != MDA_MAGIC {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// raw_locn[0]
|
||||||
|
let loc_off = u64::from_le_bytes(h[40..48].try_into().unwrap());
|
||||||
|
let loc_size = u64::from_le_bytes(h[48..56].try_into().unwrap());
|
||||||
|
if loc_size == 0 || loc_size > 16 << 20 || mda_size < 1024 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut text = vec![];
|
||||||
|
if loc_off + loc_size <= mda_size {
|
||||||
|
if let Ok(t) = read_vec(src, mda_off + loc_off, loc_size as usize) {
|
||||||
|
text = t;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// O texto dá a volta no anel.
|
||||||
|
let first = mda_size - loc_off;
|
||||||
|
if let (Ok(a), Ok(b)) = (
|
||||||
|
read_vec(src, mda_off + loc_off, first as usize),
|
||||||
|
read_vec(src, mda_off + 512, (loc_size - first) as usize),
|
||||||
|
) {
|
||||||
|
text = [a, b].concat();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let s = String::from_utf8_lossy(&text).trim_end_matches('\0').to_string();
|
||||||
|
let Some(tree) = parse(&s) else { continue };
|
||||||
|
let Some((vg_name, vg)) = tree.sections.iter().find(|(_, sec)| sec.values.contains_key("extent_size")) else { continue };
|
||||||
|
let seqno = vg.int("seqno").unwrap_or(0);
|
||||||
|
let cand = PvInfo { uuid: uuid.clone(), size, seqno, vg_name: vg_name.clone(), text: s.clone() };
|
||||||
|
if best.as_ref().map(|b| cand.seqno > b.seqno).unwrap_or(true) {
|
||||||
|
best = Some(cand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
best
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ texto
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Val {
|
||||||
|
Int(i64),
|
||||||
|
Str(String),
|
||||||
|
List(Vec<Val>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct Section {
|
||||||
|
pub values: BTreeMap<String, Val>,
|
||||||
|
/// Subseções na ordem do arquivo.
|
||||||
|
pub sections: Vec<(String, Section)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Section {
|
||||||
|
pub fn int(&self, k: &str) -> Option<u64> {
|
||||||
|
match self.values.get(k)? {
|
||||||
|
Val::Int(i) => Some(*i as u64),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn str(&self, k: &str) -> Option<&str> {
|
||||||
|
match self.values.get(k)? {
|
||||||
|
Val::Str(s) => Some(s),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn list(&self, k: &str) -> Option<&[Val]> {
|
||||||
|
match self.values.get(k)? {
|
||||||
|
Val::List(l) => Some(l),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn sub(&self, name: &str) -> Option<&Section> {
|
||||||
|
self.sections.iter().find(|(n, _)| n == name).map(|(_, s)| s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Lexer<'a> {
|
||||||
|
b: &'a [u8],
|
||||||
|
i: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Lexer<'_> {
|
||||||
|
fn skip(&mut self) {
|
||||||
|
loop {
|
||||||
|
while self.i < self.b.len() && (self.b[self.i].is_ascii_whitespace() || self.b[self.i] == b',') {
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
if self.i < self.b.len() && self.b[self.i] == b'#' {
|
||||||
|
while self.i < self.b.len() && self.b[self.i] != b'\n' {
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn ident(&mut self) -> Option<String> {
|
||||||
|
self.skip();
|
||||||
|
let s = self.i;
|
||||||
|
while self.i < self.b.len() && !self.b[self.i].is_ascii_whitespace() && !matches!(self.b[self.i], b'=' | b'{' | b'}' | b'[' | b']' | b'"' | b'#') {
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
(self.i > s).then(|| String::from_utf8_lossy(&self.b[s..self.i]).to_string())
|
||||||
|
}
|
||||||
|
fn value(&mut self) -> Option<Val> {
|
||||||
|
self.skip();
|
||||||
|
match *self.b.get(self.i)? {
|
||||||
|
b'"' => {
|
||||||
|
self.i += 1;
|
||||||
|
let s = self.i;
|
||||||
|
while self.i < self.b.len() && self.b[self.i] != b'"' {
|
||||||
|
if self.b[self.i] == b'\\' {
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
let v = String::from_utf8_lossy(&self.b[s..self.i.min(self.b.len())]).to_string();
|
||||||
|
self.i += 1;
|
||||||
|
Some(Val::Str(v))
|
||||||
|
}
|
||||||
|
b'[' => {
|
||||||
|
self.i += 1;
|
||||||
|
let mut l = vec![];
|
||||||
|
loop {
|
||||||
|
self.skip();
|
||||||
|
if *self.b.get(self.i)? == b']' {
|
||||||
|
self.i += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
l.push(self.value()?);
|
||||||
|
}
|
||||||
|
Some(Val::List(l))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let s = self.i;
|
||||||
|
while self.i < self.b.len() && (self.b[self.i].is_ascii_digit() || self.b[self.i] == b'-') {
|
||||||
|
self.i += 1;
|
||||||
|
}
|
||||||
|
if self.i == s {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
std::str::from_utf8(&self.b[s..self.i]).ok()?.parse().ok().map(Val::Int)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn section(&mut self, top: bool) -> Option<Section> {
|
||||||
|
let mut sec = Section::default();
|
||||||
|
loop {
|
||||||
|
self.skip();
|
||||||
|
if self.i >= self.b.len() {
|
||||||
|
return top.then_some(sec);
|
||||||
|
}
|
||||||
|
if self.b[self.i] == b'}' {
|
||||||
|
self.i += 1;
|
||||||
|
return (!top).then_some(sec);
|
||||||
|
}
|
||||||
|
let name = self.ident()?;
|
||||||
|
self.skip();
|
||||||
|
match *self.b.get(self.i)? {
|
||||||
|
b'=' => {
|
||||||
|
self.i += 1;
|
||||||
|
let v = self.value()?;
|
||||||
|
sec.values.insert(name, v);
|
||||||
|
}
|
||||||
|
b'{' => {
|
||||||
|
self.i += 1;
|
||||||
|
let s = self.section(false)?;
|
||||||
|
sec.sections.push((name, s));
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(text: &str) -> Option<Section> {
|
||||||
|
Lexer { b: text.as_bytes(), i: 0 }.section(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------- montagem
|
||||||
|
|
||||||
|
fn dashed(u: &str) -> String {
|
||||||
|
if u.len() != 32 {
|
||||||
|
return u.to_string();
|
||||||
|
}
|
||||||
|
format!("{}-{}-{}-{}-{}-{}-{}", &u[0..6], &u[6..10], &u[10..14], &u[14..18], &u[18..22], &u[22..26], &u[26..32])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum Map {
|
||||||
|
/// Lê de `src` a partir de `base` (linear).
|
||||||
|
Linear { src: Option<Arc<dyn Source>>, base: u64 },
|
||||||
|
Striped { srcs: Vec<(Option<Arc<dyn Source>>, u64)>, stripe: u64 },
|
||||||
|
Mirror { legs: Vec<(Option<Arc<dyn Source>>, u64)> },
|
||||||
|
Raid(Arc<RaidSource>),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Seg {
|
||||||
|
start: u64,
|
||||||
|
len: u64,
|
||||||
|
map: Map,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LvSource {
|
||||||
|
segs: Vec<Seg>,
|
||||||
|
size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Source for LvSource {
|
||||||
|
fn size(&self) -> u64 {
|
||||||
|
self.size
|
||||||
|
}
|
||||||
|
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
if offset >= self.size || buf.is_empty() {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
let want = buf.len().min((self.size - offset) as usize);
|
||||||
|
let mut done = 0;
|
||||||
|
while done < want {
|
||||||
|
let pos = offset + done as u64;
|
||||||
|
let i = self.segs.partition_point(|s| s.start + s.len <= pos);
|
||||||
|
let Some(s) = self.segs.get(i) else { break };
|
||||||
|
let rel = pos - s.start;
|
||||||
|
let mut n = ((s.len - rel) as usize).min(want - done);
|
||||||
|
let dst = &mut buf[done..done + n];
|
||||||
|
match &s.map {
|
||||||
|
Map::Linear { src, base } => match src {
|
||||||
|
Some(src) => n = read_full(src.as_ref(), base + rel, dst)?,
|
||||||
|
None => dst.fill(0),
|
||||||
|
},
|
||||||
|
Map::Striped { srcs, stripe } => {
|
||||||
|
let ns = srcs.len() as u64;
|
||||||
|
let (idx, within) = (rel / stripe, rel % stripe);
|
||||||
|
n = n.min((stripe - within) as usize);
|
||||||
|
let (src, base) = &srcs[(idx % ns) as usize];
|
||||||
|
let off = base + (idx / ns) * stripe + within;
|
||||||
|
match src {
|
||||||
|
Some(src) => n = read_full(src.as_ref(), off, &mut dst[..n])?,
|
||||||
|
None => dst[..n].fill(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map::Mirror { legs } => {
|
||||||
|
let mut ok = false;
|
||||||
|
for (src, base) in legs {
|
||||||
|
if let Some(src) = src {
|
||||||
|
if let Ok(g) = read_full(src.as_ref(), base + rel, dst) {
|
||||||
|
n = g;
|
||||||
|
ok = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
dst.fill(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map::Raid(r) => n = read_full(r.as_ref(), rel, dst)?,
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
done += n;
|
||||||
|
}
|
||||||
|
Ok(done)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_full(src: &dyn Source, off: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
let mut got = 0;
|
||||||
|
while got < buf.len() {
|
||||||
|
let n = src.read_at(off + got as u64, &mut buf[got..])?;
|
||||||
|
if n == 0 {
|
||||||
|
buf[got..].fill(0);
|
||||||
|
return Ok(buf.len());
|
||||||
|
}
|
||||||
|
got += n;
|
||||||
|
}
|
||||||
|
Ok(got)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct VgResult {
|
||||||
|
pub name: String,
|
||||||
|
pub lvs: Vec<(String, Arc<dyn Source>)>,
|
||||||
|
pub warnings: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mk_stripes(list: &[Val]) -> Vec<(String, u64)> {
|
||||||
|
let mut out = vec![];
|
||||||
|
for pair in list.chunks(2) {
|
||||||
|
if let [Val::Str(n), Val::Int(e)] = pair {
|
||||||
|
out.push((n.clone(), *e as u64));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Ctx<'a> {
|
||||||
|
ext: u64,
|
||||||
|
pvs: BTreeMap<String, (Option<Arc<dyn Source>>, u64)>, // nome -> (origem, pe_start em bytes)
|
||||||
|
lvs: &'a Section,
|
||||||
|
cache: BTreeMap<String, Option<Arc<dyn Source>>>,
|
||||||
|
warnings: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ctx<'_> {
|
||||||
|
fn build(&mut self, name: &str, depth: u32) -> Option<Arc<dyn Source>> {
|
||||||
|
if let Some(c) = self.cache.get(name) {
|
||||||
|
return c.clone();
|
||||||
|
}
|
||||||
|
if depth > 6 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let lv = self.lvs.sub(name)?.clone();
|
||||||
|
let nsegs = lv.int("segment_count").unwrap_or(0);
|
||||||
|
let mut segs: Vec<Seg> = vec![];
|
||||||
|
for i in 1..=nsegs {
|
||||||
|
let Some(seg) = lv.sub(&format!("segment{i}")) else {
|
||||||
|
self.warnings.push(format!("{name}: segmento {i} ausente"));
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let start = seg.int("start_extent").unwrap_or(0) * self.ext;
|
||||||
|
let len = seg.int("extent_count").unwrap_or(0) * self.ext;
|
||||||
|
let ty = seg.str("type").unwrap_or("").to_string();
|
||||||
|
let map = match ty.as_str() {
|
||||||
|
"striped" => {
|
||||||
|
let st = mk_stripes(seg.list("stripes").unwrap_or(&[]));
|
||||||
|
let srcs: Vec<(Option<Arc<dyn Source>>, u64)> = st
|
||||||
|
.iter()
|
||||||
|
.map(|(pv, e)| match self.pvs.get(pv) {
|
||||||
|
Some((s, pe)) => (s.clone(), pe + e * self.ext),
|
||||||
|
None => (None, 0),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if srcs.iter().any(|(s, _)| s.is_none()) {
|
||||||
|
self.warnings.push(format!("{name}: falta um volume físico; as áreas dele aparecem zeradas"));
|
||||||
|
}
|
||||||
|
if srcs.len() == 1 {
|
||||||
|
let (src, base) = srcs.into_iter().next().unwrap();
|
||||||
|
Map::Linear { src, base }
|
||||||
|
} else {
|
||||||
|
Map::Striped { srcs, stripe: seg.int("stripe_size").unwrap_or(128) * 512 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"mirror" => {
|
||||||
|
let st = mk_stripes(seg.list("mirrors").unwrap_or(&[]));
|
||||||
|
let legs = st
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(pv, e)| self.pvs.get(pv).map(|(s, pe)| (s.clone(), pe + e * self.ext)))
|
||||||
|
.collect();
|
||||||
|
Map::Mirror { legs }
|
||||||
|
}
|
||||||
|
t if t.starts_with("raid") => {
|
||||||
|
let raids = seg.list("raids").unwrap_or(&[]);
|
||||||
|
let imgs: Vec<String> = raids.iter().filter_map(|v| if let Val::Str(s) = v { Some(s.clone()) } else { None }).collect();
|
||||||
|
// Pares (rimage, rmeta) ou lista só de imagens.
|
||||||
|
let data_lvs: Vec<&String> = imgs.iter().filter(|n| n.contains("_rimage_")).collect();
|
||||||
|
let mut members: Vec<Option<Member>> = vec![];
|
||||||
|
for n in &data_lvs {
|
||||||
|
members.push(self.build(n, depth + 1).map(|src| Member { src, data_offset: 0 }));
|
||||||
|
}
|
||||||
|
let chunk = seg.int("stripe_size").unwrap_or(128) * 512;
|
||||||
|
let (level, layout) = match t {
|
||||||
|
"raid1" => (Level::R1, Layout::LeftSymmetric),
|
||||||
|
"raid0" | "raid0_meta" => (Level::R0, Layout::LeftSymmetric),
|
||||||
|
"raid4" | "raid5_n" => (Level::R4, Layout::ParityLast),
|
||||||
|
"raid5" | "raid5_ls" => (Level::R5, Layout::LeftSymmetric),
|
||||||
|
"raid5_la" => (Level::R5, Layout::LeftAsymmetric),
|
||||||
|
"raid5_rs" => (Level::R5, Layout::RightSymmetric),
|
||||||
|
"raid5_ra" => (Level::R5, Layout::RightAsymmetric),
|
||||||
|
"raid6" | "raid6_zr" | "raid6_ls_6" => (Level::R6, Layout::LeftSymmetric),
|
||||||
|
"raid6_nr" => (Level::R6, Layout::LeftAsymmetric),
|
||||||
|
"raid6_nc" | "raid6_n_6" => (Level::R6, Layout::ParityLast),
|
||||||
|
"raid10" => (Level::R10, Layout::LeftSymmetric),
|
||||||
|
other => {
|
||||||
|
self.warnings.push(format!("{name}: tipo {other} não suportado"));
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut p = RaidParams::simple(level, chunk.max(512), layout, members);
|
||||||
|
p.near = 2;
|
||||||
|
p.far = 1;
|
||||||
|
match RaidSource::new(p) {
|
||||||
|
Ok(r) => Map::Raid(Arc::new(r)),
|
||||||
|
Err(e) => {
|
||||||
|
self.warnings.push(format!("{name}: {e}"));
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
self.warnings.push(format!("{name}: tipo de segmento \"{other}\" não suportado"));
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
segs.push(Seg { start, len, map });
|
||||||
|
}
|
||||||
|
segs.sort_by_key(|s| s.start);
|
||||||
|
let size = segs.iter().map(|s| s.start + s.len).max().unwrap_or(0);
|
||||||
|
let src: Arc<dyn Source> = Arc::new(LvSource { segs, size });
|
||||||
|
self.cache.insert(name.to_string(), Some(src.clone()));
|
||||||
|
Some(src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Monta os volumes lógicos de cada grupo de volumes presente nos PVs fornecidos.
|
||||||
|
pub fn assemble_vgs(pvs: Vec<(Arc<dyn Source>, PvInfo)>) -> Vec<VgResult> {
|
||||||
|
let mut by_vg: BTreeMap<String, Vec<(Arc<dyn Source>, PvInfo)>> = BTreeMap::new();
|
||||||
|
for (s, p) in pvs {
|
||||||
|
by_vg.entry(p.vg_name.clone()).or_default().push((s, p));
|
||||||
|
}
|
||||||
|
let mut out = vec![];
|
||||||
|
for (vg_name, list) in by_vg {
|
||||||
|
let newest = list.iter().max_by_key(|(_, p)| p.seqno).unwrap();
|
||||||
|
let Some(tree) = parse(&newest.1.text) else { continue };
|
||||||
|
let Some(vg) = tree.sub(&vg_name) else { continue };
|
||||||
|
let ext = vg.int("extent_size").unwrap_or(8192) * 512;
|
||||||
|
let mut pvmap = BTreeMap::new();
|
||||||
|
let mut warnings = vec![];
|
||||||
|
if let Some(pvsec) = vg.sub("physical_volumes") {
|
||||||
|
for (pvname, def) in &pvsec.sections {
|
||||||
|
let id = def.str("id").unwrap_or("").to_string();
|
||||||
|
let found = list.iter().find(|(_, p)| dashed(&p.uuid) == id).map(|(s, _)| s.clone());
|
||||||
|
if found.is_none() {
|
||||||
|
warnings.push(format!("volume físico {pvname} ({id}) não foi fornecido"));
|
||||||
|
}
|
||||||
|
pvmap.insert(pvname.clone(), (found, def.int("pe_start").unwrap_or(2048) * 512));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some(lvsec) = vg.sub("logical_volumes") else { continue };
|
||||||
|
let mut ctx = Ctx { ext, pvs: pvmap, lvs: lvsec, cache: BTreeMap::new(), warnings };
|
||||||
|
let mut lvs = vec![];
|
||||||
|
for (name, def) in &lvsec.sections {
|
||||||
|
// Imagens internas do RAID e metadados não são volumes de usuário.
|
||||||
|
let hidden = def.list("status").map(|l| !l.iter().any(|v| matches!(v, Val::Str(s) if s == "VISIBLE"))).unwrap_or(false);
|
||||||
|
if hidden {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(src) = ctx.build(name, 0) {
|
||||||
|
lvs.push((name.clone(), src));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push(VgResult { name: vg_name, lvs, warnings: ctx.warnings });
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
@@ -0,0 +1,690 @@
|
|||||||
|
//! RAID por software: linear, 0, 1, 4, 5, 6 e 10, a partir dos discos soltos.
|
||||||
|
//!
|
||||||
|
//! Lê os metadados do mdadm (superbloco 0.90 e 1.x) para montar sozinho; sem metadados, o
|
||||||
|
//! módulo `raid_detect` descobre ordem, tamanho de faixa e layout. Se faltarem discos, o
|
||||||
|
//! array é lido em modo degradado (paridade reconstrói o que faltar).
|
||||||
|
|
||||||
|
use crate::source::{read_vec, Source};
|
||||||
|
use std::io;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum Level {
|
||||||
|
Linear,
|
||||||
|
R0,
|
||||||
|
R1,
|
||||||
|
R4,
|
||||||
|
R5,
|
||||||
|
R6,
|
||||||
|
R10,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Level {
|
||||||
|
pub fn name(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Level::Linear => "linear",
|
||||||
|
Level::R0 => "RAID 0",
|
||||||
|
Level::R1 => "RAID 1",
|
||||||
|
Level::R4 => "RAID 4",
|
||||||
|
Level::R5 => "RAID 5",
|
||||||
|
Level::R6 => "RAID 6",
|
||||||
|
Level::R10 => "RAID 10",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Posição da paridade e ordem dos dados, como no driver md do Linux.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum Layout {
|
||||||
|
LeftAsymmetric,
|
||||||
|
RightAsymmetric,
|
||||||
|
LeftSymmetric,
|
||||||
|
RightSymmetric,
|
||||||
|
/// RAID 4 e variantes "parity-0"/"parity-n".
|
||||||
|
ParityFirst,
|
||||||
|
ParityLast,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Layout {
|
||||||
|
pub fn from_md(v: u32) -> Layout {
|
||||||
|
match v {
|
||||||
|
0 => Layout::LeftAsymmetric,
|
||||||
|
1 => Layout::RightAsymmetric,
|
||||||
|
2 => Layout::LeftSymmetric,
|
||||||
|
3 => Layout::RightSymmetric,
|
||||||
|
4 => Layout::ParityFirst,
|
||||||
|
5 => Layout::ParityLast,
|
||||||
|
_ => Layout::LeftSymmetric,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn all_raid5() -> [Layout; 4] {
|
||||||
|
[Layout::LeftSymmetric, Layout::LeftAsymmetric, Layout::RightSymmetric, Layout::RightAsymmetric]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Member {
|
||||||
|
pub src: Arc<dyn Source>,
|
||||||
|
/// Onde começam os dados dentro do membro.
|
||||||
|
pub data_offset: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RaidParams {
|
||||||
|
pub level: Level,
|
||||||
|
/// Tamanho da faixa por disco, em bytes.
|
||||||
|
pub chunk: u64,
|
||||||
|
pub layout: Layout,
|
||||||
|
/// Membros na ordem do array; `None` = disco ausente.
|
||||||
|
pub members: Vec<Option<Member>>,
|
||||||
|
/// RAID 10: cópias "perto" (near) e "longe" (far).
|
||||||
|
pub near: u32,
|
||||||
|
pub far: u32,
|
||||||
|
pub far_offset: bool,
|
||||||
|
/// Bytes úteis por membro (0 = até o fim do menor).
|
||||||
|
pub per_member: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RaidParams {
|
||||||
|
pub fn simple(level: Level, chunk: u64, layout: Layout, members: Vec<Option<Member>>) -> Self {
|
||||||
|
Self { level, chunk, layout, members, near: 2, far: 1, far_offset: false, per_member: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RaidSource {
|
||||||
|
p: RaidParams,
|
||||||
|
size: u64,
|
||||||
|
per_member: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------ GF(2^8) para RAID 6
|
||||||
|
|
||||||
|
fn gf_mul(mut a: u8, mut b: u8) -> u8 {
|
||||||
|
let mut r = 0u8;
|
||||||
|
while b != 0 {
|
||||||
|
if b & 1 != 0 {
|
||||||
|
r ^= a;
|
||||||
|
}
|
||||||
|
let hi = a & 0x80;
|
||||||
|
a <<= 1;
|
||||||
|
if hi != 0 {
|
||||||
|
a ^= 0x1d;
|
||||||
|
}
|
||||||
|
b >>= 1;
|
||||||
|
}
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gf_pow2(n: usize) -> u8 {
|
||||||
|
let mut r = 1u8;
|
||||||
|
for _ in 0..n % 255 {
|
||||||
|
r = gf_mul(r, 2);
|
||||||
|
}
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gf_inv(a: u8) -> u8 {
|
||||||
|
// a^254
|
||||||
|
let mut r = 1u8;
|
||||||
|
let mut base = a;
|
||||||
|
let mut e = 254u32;
|
||||||
|
while e > 0 {
|
||||||
|
if e & 1 != 0 {
|
||||||
|
r = gf_mul(r, base);
|
||||||
|
}
|
||||||
|
base = gf_mul(base, base);
|
||||||
|
e >>= 1;
|
||||||
|
}
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RaidSource {
|
||||||
|
pub fn new(p: RaidParams) -> Result<Self, String> {
|
||||||
|
let n = p.members.len();
|
||||||
|
if n == 0 {
|
||||||
|
return Err("RAID sem membros".into());
|
||||||
|
}
|
||||||
|
let present: Vec<&Member> = p.members.iter().flatten().collect();
|
||||||
|
let min = |f: &dyn Fn(&Member) -> u64| present.iter().map(|m| f(m)).min().unwrap_or(0);
|
||||||
|
let avail = min(&|m| m.src.size().saturating_sub(m.data_offset));
|
||||||
|
let mut per_member = if p.per_member > 0 { p.per_member.min(avail) } else { avail };
|
||||||
|
if p.level != Level::Linear {
|
||||||
|
per_member = per_member / p.chunk.max(512) * p.chunk.max(512);
|
||||||
|
}
|
||||||
|
let missing = n - present.len();
|
||||||
|
let need = match p.level {
|
||||||
|
Level::Linear | Level::R0 => 0,
|
||||||
|
Level::R1 => n - 1,
|
||||||
|
Level::R4 | Level::R5 => 1,
|
||||||
|
Level::R6 => 2,
|
||||||
|
Level::R10 => (p.near.max(1) as usize).saturating_sub(1).min(1),
|
||||||
|
};
|
||||||
|
if missing > need && p.level != Level::R10 {
|
||||||
|
return Err(format!("faltam {missing} membro(s); {} tolera no máximo {need}", p.level.name()));
|
||||||
|
}
|
||||||
|
if matches!(p.level, Level::R4 | Level::R5) && n < 3 || p.level == Level::R6 && n < 4 {
|
||||||
|
return Err("poucos membros para o nível".into());
|
||||||
|
}
|
||||||
|
if p.level == Level::R10 && (n < 2 || p.near as usize * p.far as usize > n.max(2)) {
|
||||||
|
return Err("parâmetros de RAID 10 inválidos".into());
|
||||||
|
}
|
||||||
|
let size = match p.level {
|
||||||
|
Level::Linear => present.iter().map(|m| m.src.size().saturating_sub(m.data_offset)).sum(),
|
||||||
|
Level::R0 => per_member * n as u64,
|
||||||
|
Level::R1 => per_member,
|
||||||
|
Level::R4 | Level::R5 => per_member * (n as u64 - 1),
|
||||||
|
Level::R6 => per_member * (n as u64 - 2),
|
||||||
|
Level::R10 => per_member * n as u64 / (p.near.max(1) * p.far.max(1)) as u64,
|
||||||
|
};
|
||||||
|
Ok(Self { p, size, per_member })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn params(&self) -> &RaidParams {
|
||||||
|
&self.p
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_member(&self, i: usize, off: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||||
|
let m = self.p.members[i].as_ref().ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "membro ausente"))?;
|
||||||
|
let mut done = 0;
|
||||||
|
while done < buf.len() {
|
||||||
|
let n = m.src.read_at(m.data_offset + off + done as u64, &mut buf[done..])?;
|
||||||
|
if n == 0 {
|
||||||
|
buf[done..].fill(0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
done += n;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (disco de paridade P, disco de paridade Q, disco que guarda o dado `d` da linha `row`)
|
||||||
|
fn raid5_map(&self, row: u64, d: usize) -> (usize, usize) {
|
||||||
|
let n = self.p.members.len();
|
||||||
|
let r = (row % n as u64) as usize;
|
||||||
|
match self.p.layout {
|
||||||
|
Layout::LeftAsymmetric | Layout::LeftSymmetric => {
|
||||||
|
let pd = n - 1 - r;
|
||||||
|
let disk = if self.p.layout == Layout::LeftSymmetric { (pd + 1 + d) % n } else if d >= pd { d + 1 } else { d };
|
||||||
|
(pd, disk)
|
||||||
|
}
|
||||||
|
Layout::RightAsymmetric | Layout::RightSymmetric => {
|
||||||
|
let pd = r;
|
||||||
|
let disk = if self.p.layout == Layout::RightSymmetric { (pd + 1 + d) % n } else if d >= pd { d + 1 } else { d };
|
||||||
|
(pd, disk)
|
||||||
|
}
|
||||||
|
Layout::ParityFirst => (0, d + 1),
|
||||||
|
Layout::ParityLast => (n - 1, d),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raid6_map(&self, row: u64, d: usize) -> (usize, usize, usize) {
|
||||||
|
let n = self.p.members.len();
|
||||||
|
let r = (row % n as u64) as usize;
|
||||||
|
match self.p.layout {
|
||||||
|
Layout::LeftAsymmetric | Layout::LeftSymmetric => {
|
||||||
|
let pd = n - 1 - r;
|
||||||
|
let qd = (pd + 1) % n;
|
||||||
|
let disk = if self.p.layout == Layout::LeftSymmetric {
|
||||||
|
(pd + 2 + d) % n
|
||||||
|
} else {
|
||||||
|
// assimétrico: dados em ordem pulando P e Q
|
||||||
|
let mut k = d;
|
||||||
|
for skip in sorted2(pd, qd) {
|
||||||
|
if k >= skip {
|
||||||
|
k += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
k
|
||||||
|
};
|
||||||
|
(pd, qd, disk)
|
||||||
|
}
|
||||||
|
Layout::RightAsymmetric | Layout::RightSymmetric => {
|
||||||
|
let pd = r;
|
||||||
|
let qd = (pd + 1) % n;
|
||||||
|
let disk = if self.p.layout == Layout::RightSymmetric {
|
||||||
|
(pd + 2 + d) % n
|
||||||
|
} else {
|
||||||
|
let mut k = d;
|
||||||
|
for skip in sorted2(pd, qd) {
|
||||||
|
if k >= skip {
|
||||||
|
k += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
k
|
||||||
|
};
|
||||||
|
(pd, qd, disk)
|
||||||
|
}
|
||||||
|
Layout::ParityFirst => (0, 1, d + 2),
|
||||||
|
Layout::ParityLast => (n - 2, n - 1, d),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lê `buf.len()` bytes dentro de um único chunk lógico.
|
||||||
|
fn read_in_chunk(&self, chunk_idx: u64, within: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||||
|
let n = self.p.members.len();
|
||||||
|
let c = self.p.chunk;
|
||||||
|
match self.p.level {
|
||||||
|
Level::Linear => unreachable!(),
|
||||||
|
Level::R0 => {
|
||||||
|
let (row, disk) = (chunk_idx / n as u64, (chunk_idx % n as u64) as usize);
|
||||||
|
self.read_member(disk, row * c + within, buf)
|
||||||
|
}
|
||||||
|
Level::R1 => {
|
||||||
|
for i in 0..n {
|
||||||
|
if self.p.members[i].is_some() && self.read_member(i, chunk_idx * c + within, buf).is_ok() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(io::Error::new(io::ErrorKind::Other, "nenhuma cópia legível"))
|
||||||
|
}
|
||||||
|
Level::R4 | Level::R5 => {
|
||||||
|
let dd = n - 1;
|
||||||
|
let (row, d) = (chunk_idx / dd as u64, (chunk_idx % dd as u64) as usize);
|
||||||
|
let (pd, disk) = self.raid5_map(row, d);
|
||||||
|
let off = row * c + within;
|
||||||
|
if self.p.members[disk].is_some() {
|
||||||
|
return self.read_member(disk, off, buf);
|
||||||
|
}
|
||||||
|
// Reconstrói por XOR de todos os outros (dados + paridade) da linha.
|
||||||
|
buf.fill(0);
|
||||||
|
let mut tmp = vec![0u8; buf.len()];
|
||||||
|
for i in 0..n {
|
||||||
|
if i == disk {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let _ = pd;
|
||||||
|
self.read_member(i, off, &mut tmp)?;
|
||||||
|
for (a, b) in buf.iter_mut().zip(&tmp) {
|
||||||
|
*a ^= *b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Level::R6 => {
|
||||||
|
let dd = n - 2;
|
||||||
|
let (row, d) = (chunk_idx / dd as u64, (chunk_idx % dd as u64) as usize);
|
||||||
|
let (_, _, disk) = self.raid6_map(row, d);
|
||||||
|
let off = row * c + within;
|
||||||
|
if self.p.members[disk].is_some() {
|
||||||
|
return self.read_member(disk, off, buf);
|
||||||
|
}
|
||||||
|
self.raid6_rebuild(row, d, off, buf)
|
||||||
|
}
|
||||||
|
Level::R10 => {
|
||||||
|
let mut last = None;
|
||||||
|
for (disk, off) in self.r10_locations(chunk_idx) {
|
||||||
|
if self.p.members[disk].is_some() {
|
||||||
|
match self.read_member(disk, off + within, buf) {
|
||||||
|
Ok(()) => return Ok(()),
|
||||||
|
Err(e) => last = Some(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(last.unwrap_or_else(|| io::Error::new(io::ErrorKind::NotFound, "cópias ausentes")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Onde ficam todas as cópias de um chunk lógico (como `raid10_find_phys` do kernel).
|
||||||
|
fn r10_locations(&self, chunk_idx: u64) -> Vec<(usize, u64)> {
|
||||||
|
let n = self.p.members.len() as u64;
|
||||||
|
let c = self.p.chunk;
|
||||||
|
let near = self.p.near.max(1) as u64;
|
||||||
|
let far = self.p.far.max(1) as u64;
|
||||||
|
let slot0 = chunk_idx * near;
|
||||||
|
let (mut stripe, dev) = (slot0 / n, slot0 % n);
|
||||||
|
if self.p.far_offset {
|
||||||
|
stripe *= far;
|
||||||
|
}
|
||||||
|
let stride = if self.p.far_offset { c } else { (self.per_member / far) / c * c };
|
||||||
|
let mut out = vec![];
|
||||||
|
let mut d = dev;
|
||||||
|
let mut off = stripe * c;
|
||||||
|
let mut firsts = vec![];
|
||||||
|
for _ in 0..near {
|
||||||
|
firsts.push((d, off));
|
||||||
|
out.push((d as usize, off));
|
||||||
|
d += 1;
|
||||||
|
if d >= n {
|
||||||
|
d = 0;
|
||||||
|
off += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for f in 1..far {
|
||||||
|
for &(dv, of) in &firsts {
|
||||||
|
out.push((((dv + near * f) % n) as usize, of + stride * f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raid6_rebuild(&self, row: u64, d: usize, off: u64, buf: &mut [u8]) -> io::Result<()> {
|
||||||
|
let n = self.p.members.len();
|
||||||
|
let dd = n - 2;
|
||||||
|
let (pd, qd, _) = self.raid6_map(row, 0);
|
||||||
|
let len = buf.len();
|
||||||
|
// Estado de cada dado da linha.
|
||||||
|
let disks: Vec<usize> = (0..dd).map(|k| self.raid6_map(row, k).2).collect();
|
||||||
|
let missing_data: Vec<usize> = (0..dd).filter(|&k| self.p.members[disks[k]].is_none()).collect();
|
||||||
|
let p_ok = self.p.members[pd].is_some();
|
||||||
|
let q_ok = self.p.members[qd].is_some();
|
||||||
|
let mut data: Vec<Option<Vec<u8>>> = vec![None; dd];
|
||||||
|
for k in 0..dd {
|
||||||
|
if self.p.members[disks[k]].is_some() {
|
||||||
|
let mut v = vec![0u8; len];
|
||||||
|
self.read_member(disks[k], off, &mut v)?;
|
||||||
|
data[k] = Some(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let rd = |disk: usize| -> io::Result<Vec<u8>> {
|
||||||
|
let mut v = vec![0u8; len];
|
||||||
|
self.read_member(disk, off, &mut v)?;
|
||||||
|
Ok(v)
|
||||||
|
};
|
||||||
|
if missing_data.len() == 1 && p_ok {
|
||||||
|
let p = rd(pd)?;
|
||||||
|
let mut acc = p;
|
||||||
|
for v in data.iter().flatten() {
|
||||||
|
for (a, b) in acc.iter_mut().zip(v) {
|
||||||
|
*a ^= *b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.copy_from_slice(&acc);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if missing_data.len() == 1 && q_ok {
|
||||||
|
let q = rd(qd)?;
|
||||||
|
let x = missing_data[0];
|
||||||
|
let mut acc = q;
|
||||||
|
for (k, v) in data.iter().enumerate() {
|
||||||
|
if let Some(v) = v {
|
||||||
|
let g = gf_pow2(k);
|
||||||
|
for (a, b) in acc.iter_mut().zip(v) {
|
||||||
|
*a ^= gf_mul(g, *b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let inv = gf_inv(gf_pow2(x));
|
||||||
|
for (o, a) in buf.iter_mut().zip(&acc) {
|
||||||
|
*o = gf_mul(inv, *a);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if missing_data.len() == 2 && p_ok && q_ok {
|
||||||
|
let (x, y) = (missing_data[0], missing_data[1]);
|
||||||
|
let p = rd(pd)?;
|
||||||
|
let q = rd(qd)?;
|
||||||
|
let (mut pxy, mut qxy) = (p, q);
|
||||||
|
for (k, v) in data.iter().enumerate() {
|
||||||
|
if let Some(v) = v {
|
||||||
|
let g = gf_pow2(k);
|
||||||
|
for i in 0..len {
|
||||||
|
pxy[i] ^= v[i];
|
||||||
|
qxy[i] ^= gf_mul(g, v[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Dx ^ Dy = pxy ; g^x Dx ^ g^y Dy = qxy
|
||||||
|
let (gx, gy) = (gf_pow2(x), gf_pow2(y));
|
||||||
|
let denom_inv = gf_inv(gx ^ gy);
|
||||||
|
let target = if d == x { x } else { y };
|
||||||
|
for i in 0..len {
|
||||||
|
// Dx = (qxy ^ gy*pxy) / (gx ^ gy)
|
||||||
|
let dx = gf_mul(denom_inv, qxy[i] ^ gf_mul(gy, pxy[i]));
|
||||||
|
buf[i] = if target == x { dx } else { pxy[i] ^ dx };
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(io::Error::new(io::ErrorKind::Other, "RAID 6 sem redundância suficiente nesta linha"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sorted2(a: usize, b: usize) -> [usize; 2] {
|
||||||
|
if a < b {
|
||||||
|
[a, b]
|
||||||
|
} else {
|
||||||
|
[b, a]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Source for RaidSource {
|
||||||
|
fn size(&self) -> u64 {
|
||||||
|
self.size
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
if offset >= self.size || buf.is_empty() {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
let want = buf.len().min((self.size - offset) as usize);
|
||||||
|
let mut done = 0usize;
|
||||||
|
if self.p.level == Level::Linear {
|
||||||
|
let mut base = 0u64;
|
||||||
|
for m in self.p.members.iter().flatten() {
|
||||||
|
let len = m.src.size().saturating_sub(m.data_offset);
|
||||||
|
if offset + done as u64 >= base && offset + (done as u64) < base + len {
|
||||||
|
let rel = offset + done as u64 - base;
|
||||||
|
let n = ((len - rel) as usize).min(want - done);
|
||||||
|
let got = m.src.read_at(m.data_offset + rel, &mut buf[done..done + n])?;
|
||||||
|
if got == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
done += got;
|
||||||
|
if done >= want {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base += len;
|
||||||
|
}
|
||||||
|
return Ok(done);
|
||||||
|
}
|
||||||
|
let c = self.p.chunk;
|
||||||
|
while done < want {
|
||||||
|
let pos = offset + done as u64;
|
||||||
|
let (idx, within) = (pos / c, pos % c);
|
||||||
|
let n = ((c - within) as usize).min(want - done);
|
||||||
|
self.read_in_chunk(idx, within, &mut buf[done..done + n])?;
|
||||||
|
done += n;
|
||||||
|
}
|
||||||
|
Ok(done)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================ metadados do mdadm
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct MdSuper {
|
||||||
|
pub version: String,
|
||||||
|
pub uuid: [u8; 16],
|
||||||
|
pub name: String,
|
||||||
|
pub level: Level,
|
||||||
|
pub layout: u32,
|
||||||
|
pub chunk: u64,
|
||||||
|
pub raid_disks: u32,
|
||||||
|
/// Papel deste disco no array (None = reserva/falho).
|
||||||
|
pub role: Option<u32>,
|
||||||
|
pub data_offset: u64,
|
||||||
|
/// Bytes de dados por membro.
|
||||||
|
pub data_size: u64,
|
||||||
|
pub events: u64,
|
||||||
|
pub array_size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn le32(b: &[u8], o: usize) -> u32 {
|
||||||
|
u32::from_le_bytes(b[o..o + 4].try_into().unwrap())
|
||||||
|
}
|
||||||
|
fn le64(b: &[u8], o: usize) -> u64 {
|
||||||
|
u64::from_le_bytes(b[o..o + 8].try_into().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn level_from(v: i32) -> Option<Level> {
|
||||||
|
Some(match v {
|
||||||
|
-1 => Level::Linear,
|
||||||
|
0 => Level::R0,
|
||||||
|
1 => Level::R1,
|
||||||
|
4 => Level::R4,
|
||||||
|
5 => Level::R5,
|
||||||
|
6 => Level::R6,
|
||||||
|
10 => Level::R10,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Procura um superbloco md nas posições possíveis de `src` (um disco ou partição).
|
||||||
|
pub fn read_md_super(src: &dyn Source) -> Option<MdSuper> {
|
||||||
|
let size = src.size();
|
||||||
|
// 1.1 no início, 1.2 a 4 KiB, 1.0 a 8 KiB do fim.
|
||||||
|
let mut cands: Vec<(u64, &str)> = vec![(0, "1.1"), (4096, "1.2")];
|
||||||
|
if size > 16384 {
|
||||||
|
cands.push(((size & !4095) - 8192, "1.0"));
|
||||||
|
}
|
||||||
|
for (off, ver) in cands {
|
||||||
|
let Ok(b) = read_vec(src, off, 4096) else { continue };
|
||||||
|
if b.len() < 4096 || le32(&b, 0) != 0xA92B_4EFC || le32(&b, 4) != 1 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let level = level_from(le32(&b, 72) as i32)?;
|
||||||
|
let max_dev = le32(&b, 220) as usize;
|
||||||
|
let dev_number = le32(&b, 160) as usize;
|
||||||
|
let role = if dev_number < max_dev.min(1024) && 256 + dev_number * 2 + 2 <= b.len() {
|
||||||
|
let r = u16::from_le_bytes([b[256 + dev_number * 2], b[256 + dev_number * 2 + 1]]);
|
||||||
|
(r < 0xFFFE).then_some(r as u32)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let mut uuid = [0u8; 16];
|
||||||
|
uuid.copy_from_slice(&b[16..32]);
|
||||||
|
let name = String::from_utf8_lossy(&b[32..64]).trim_end_matches('\0').to_string();
|
||||||
|
let data_offset = le64(&b, 128) * 512;
|
||||||
|
let data_size = le64(&b, 136) * 512;
|
||||||
|
let raid_disks = le32(&b, 92);
|
||||||
|
let chunk = le32(&b, 88) as u64 * 512;
|
||||||
|
let used = le64(&b, 80) * 512;
|
||||||
|
let array_size = match level {
|
||||||
|
Level::R0 => used * raid_disks as u64,
|
||||||
|
Level::R5 | Level::R4 => used * (raid_disks as u64).saturating_sub(1),
|
||||||
|
Level::R6 => used * (raid_disks as u64).saturating_sub(2),
|
||||||
|
_ => used,
|
||||||
|
};
|
||||||
|
return Some(MdSuper {
|
||||||
|
version: ver.into(),
|
||||||
|
uuid,
|
||||||
|
name,
|
||||||
|
level,
|
||||||
|
layout: le32(&b, 76),
|
||||||
|
chunk,
|
||||||
|
raid_disks,
|
||||||
|
role,
|
||||||
|
data_offset,
|
||||||
|
data_size,
|
||||||
|
events: le64(&b, 200),
|
||||||
|
array_size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 0.90: no fim, alinhado a 64 KiB.
|
||||||
|
if size > 131072 {
|
||||||
|
let off = (size & !65535) - 65536;
|
||||||
|
if let Ok(b) = read_vec(src, off, 4096) {
|
||||||
|
if b.len() >= 1024 && le32(&b, 0) == 0xA92B_4EFC && le32(&b, 4) == 0 {
|
||||||
|
let level = level_from(le32(&b, 28) as i32)?;
|
||||||
|
let mut uuid = [0u8; 16];
|
||||||
|
uuid[0..4].copy_from_slice(&b[20..24]);
|
||||||
|
uuid[4..16].copy_from_slice(&b[52..64]);
|
||||||
|
let role = {
|
||||||
|
let r = le32(&b, 768 + 12);
|
||||||
|
(r < 0xFFFE).then_some(r)
|
||||||
|
};
|
||||||
|
let raid_disks = le32(&b, 40);
|
||||||
|
let used = le32(&b, 32) as u64 * 1024;
|
||||||
|
return Some(MdSuper {
|
||||||
|
version: "0.90".into(),
|
||||||
|
uuid,
|
||||||
|
name: String::new(),
|
||||||
|
level,
|
||||||
|
layout: le32(&b, 256),
|
||||||
|
chunk: le32(&b, 260) as u64,
|
||||||
|
raid_disks,
|
||||||
|
role,
|
||||||
|
data_offset: 0,
|
||||||
|
data_size: used,
|
||||||
|
events: ((le32(&b, 39 * 4) as u64) << 32) | le32(&b, 40 * 4) as u64,
|
||||||
|
array_size: match level {
|
||||||
|
Level::R0 => used * raid_disks as u64,
|
||||||
|
Level::R5 | Level::R4 => used * (raid_disks as u64).saturating_sub(1),
|
||||||
|
Level::R6 => used * (raid_disks as u64).saturating_sub(2),
|
||||||
|
_ => used,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uuid_str(u: &[u8; 16]) -> String {
|
||||||
|
u.iter().map(|b| format!("{b:02x}")).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Um array reconhecido pelos superblocos dos discos fornecidos.
|
||||||
|
pub struct FoundArray {
|
||||||
|
pub uuid: String,
|
||||||
|
pub name: String,
|
||||||
|
pub info: MdSuper,
|
||||||
|
pub source: Arc<RaidSource>,
|
||||||
|
pub missing: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Agrupa membros por UUID e monta cada array (degradado se for possível).
|
||||||
|
pub fn assemble_md(members: Vec<(Arc<dyn Source>, MdSuper)>) -> Vec<Result<FoundArray, String>> {
|
||||||
|
let mut groups: Vec<(String, Vec<(Arc<dyn Source>, MdSuper)>)> = vec![];
|
||||||
|
for (s, m) in members {
|
||||||
|
let key = uuid_str(&m.uuid);
|
||||||
|
match groups.iter_mut().find(|g| g.0 == key) {
|
||||||
|
Some(g) => g.1.push((s, m)),
|
||||||
|
None => groups.push((key, vec![(s, m)])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut out = vec![];
|
||||||
|
for (uuid, mut list) in groups {
|
||||||
|
// O membro com mais eventos manda; membros desatualizados são tratados como ausentes.
|
||||||
|
list.sort_by_key(|(_, m)| std::cmp::Reverse(m.events));
|
||||||
|
let top = list[0].1.clone();
|
||||||
|
let n = top.raid_disks as usize;
|
||||||
|
let mut slots: Vec<Option<Member>> = vec![None; n];
|
||||||
|
let mut stale = 0;
|
||||||
|
for (s, m) in &list {
|
||||||
|
if let Some(r) = m.role {
|
||||||
|
if (r as usize) < n && slots[r as usize].is_none() {
|
||||||
|
if m.events + 1 < top.events && top.events > 0 {
|
||||||
|
stale += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
slots[r as usize] = Some(Member { src: s.clone(), data_offset: m.data_offset });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let missing = slots.iter().filter(|s| s.is_none()).count();
|
||||||
|
let mut p = RaidParams::simple(top.level, top.chunk.max(512), Layout::from_md(top.layout), slots);
|
||||||
|
if top.level == Level::R10 {
|
||||||
|
p.near = top.layout & 0xff;
|
||||||
|
p.far = (top.layout >> 8) & 0xff;
|
||||||
|
p.far_offset = top.layout & 0x10000 != 0;
|
||||||
|
}
|
||||||
|
if top.data_size > 0 {
|
||||||
|
p.per_member = top.data_size;
|
||||||
|
}
|
||||||
|
match RaidSource::new(p) {
|
||||||
|
Ok(src) => out.push(Ok(FoundArray {
|
||||||
|
uuid: uuid.clone(),
|
||||||
|
name: top.name.clone(),
|
||||||
|
info: top,
|
||||||
|
source: Arc::new(src),
|
||||||
|
missing: missing + stale,
|
||||||
|
})),
|
||||||
|
Err(e) => out.push(Err(format!("array {uuid}: {e}"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
@@ -298,7 +298,7 @@ fn build_ranges(
|
|||||||
let free = merge_extents(free.to_vec());
|
let free = merge_extents(free.to_vec());
|
||||||
let mut out: Vec<Extent> = vec![];
|
let mut out: Vec<Extent> = vec![];
|
||||||
for p in &layout.partitions {
|
for p in &layout.partitions {
|
||||||
if p.encrypted.is_some() {
|
if p.encrypted.is_some() || opts.covered.contains(&p.start) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let whole = Extent::new(p.start, p.len.min(disk_size.saturating_sub(p.start)));
|
let whole = Extent::new(p.start, p.len.min(disk_size.saturating_sub(p.start)));
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ impl Engine {
|
|||||||
let fp = crate::source::fingerprint(raw.as_ref());
|
let fp = crate::source::fingerprint(raw.as_ref());
|
||||||
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();
|
||||||
if opts.assembly.remember_keys {
|
if opts.assembly.remember_keys {
|
||||||
opts.assembly.keys = asm.keys.clone();
|
opts.assembly.keys = asm.keys.clone();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -324,6 +324,9 @@ pub struct ScanOptions {
|
|||||||
/// Partições que não estão na tabela do disco (volumes lógicos, desbloqueados).
|
/// Partições que não estão na tabela do disco (volumes lógicos, desbloqueados).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub extra_parts: Vec<PartInfo>,
|
pub extra_parts: Vec<PartInfo>,
|
||||||
|
/// Partições que só contêm RAID/LVM remontado: a varredura entra pelos volumes virtuais.
|
||||||
|
#[serde(default)]
|
||||||
|
pub covered: Vec<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_max_carve() -> u64 {
|
fn default_max_carve() -> u64 {
|
||||||
@@ -339,6 +342,7 @@ impl Default for ScanOptions {
|
|||||||
max_carve_size: default_max_carve(),
|
max_carve_size: default_max_carve(),
|
||||||
assembly: Default::default(),
|
assembly: Default::default(),
|
||||||
extra_parts: vec![],
|
extra_parts: vec![],
|
||||||
|
covered: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
//! Ponta a ponta: RAID 5 de 4 discos (um perdido) com ext4 dentro e um arquivo apagado.
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
use common::*;
|
||||||
|
use guara_core::assemble::{assemble_source, Assembly};
|
||||||
|
use guara_core::scan::{self, Outcome};
|
||||||
|
use guara_core::source::{FileSource, Source};
|
||||||
|
use guara_core::*;
|
||||||
|
use std::path::Path;
|
||||||
|
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 data(seed: u64, n: usize) -> Vec<u8> {
|
||||||
|
let mut x = seed | 1;
|
||||||
|
(0..n)
|
||||||
|
.map(|_| {
|
||||||
|
x ^= x << 13;
|
||||||
|
x ^= x >> 7;
|
||||||
|
x ^= x << 17;
|
||||||
|
(x >> 24) as u8
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHUNK: usize = 64 * 1024;
|
||||||
|
|
||||||
|
fn md_super(n: u32, dev: u32, data_off: u64, sectors: u64) -> Vec<u8> {
|
||||||
|
let mut b = vec![0u8; 4096];
|
||||||
|
b[0..4].copy_from_slice(&0xA92B_4EFCu32.to_le_bytes());
|
||||||
|
b[4..8].copy_from_slice(&1u32.to_le_bytes());
|
||||||
|
b[16..32].fill(0x5a);
|
||||||
|
b[72..76].copy_from_slice(&5u32.to_le_bytes());
|
||||||
|
b[76..80].copy_from_slice(&2u32.to_le_bytes());
|
||||||
|
b[80..88].copy_from_slice(§ors.to_le_bytes());
|
||||||
|
b[88..92].copy_from_slice(&((CHUNK / 512) as u32).to_le_bytes());
|
||||||
|
b[92..96].copy_from_slice(&n.to_le_bytes());
|
||||||
|
b[128..136].copy_from_slice(&data_off.to_le_bytes());
|
||||||
|
b[136..144].copy_from_slice(§ors.to_le_bytes());
|
||||||
|
b[160..164].copy_from_slice(&dev.to_le_bytes());
|
||||||
|
b[200..208].copy_from_slice(&50u64.to_le_bytes());
|
||||||
|
b[220..224].copy_from_slice(&n.to_le_bytes());
|
||||||
|
for i in 0..n as usize {
|
||||||
|
b[256 + i * 2..258 + i * 2].copy_from_slice(&(i as u16).to_le_bytes());
|
||||||
|
}
|
||||||
|
b
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deleted_file_on_degraded_raid5() {
|
||||||
|
if !(have("mke2fs") && have("debugfs")) {
|
||||||
|
eprintln!("SKIP: e2fsprogs ausente");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
init();
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let root = dir.path().join("root");
|
||||||
|
std::fs::create_dir_all(root.join("fotos")).unwrap();
|
||||||
|
let doc = data(5, 300_000);
|
||||||
|
std::fs::write(root.join("fotos/ferias.dat"), &doc).unwrap();
|
||||||
|
std::fs::write(root.join("fotos/outra.dat"), data(6, 9000)).unwrap();
|
||||||
|
let plain = dir.path().join("plain.img");
|
||||||
|
let n = 4usize;
|
||||||
|
let dd = n - 1;
|
||||||
|
let rows = 40usize;
|
||||||
|
let logical_len = rows * dd * CHUNK;
|
||||||
|
std::fs::File::create(&plain).unwrap().set_len(logical_len 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 /fotos/ferias.dat").arg(&plain).output().unwrap();
|
||||||
|
let logical = std::fs::read(&plain).unwrap();
|
||||||
|
|
||||||
|
// RAID 5 left-symmetric
|
||||||
|
let data_off = 2048u64; // setores
|
||||||
|
let mut disks = vec![vec![0u8; data_off as usize * 512 + rows * CHUNK]; n];
|
||||||
|
for row in 0..rows {
|
||||||
|
let pd = n - 1 - row % n;
|
||||||
|
let mut p = vec![0u8; CHUNK];
|
||||||
|
for d in 0..dd {
|
||||||
|
let disk = (pd + 1 + d) % n;
|
||||||
|
let src = &logical[(row * dd + d) * CHUNK..(row * dd + d + 1) * CHUNK];
|
||||||
|
disks[disk][data_off as usize * 512 + row * CHUNK..][..CHUNK].copy_from_slice(src);
|
||||||
|
for i in 0..CHUNK {
|
||||||
|
p[i] ^= src[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disks[pd][data_off as usize * 512 + row * CHUNK..][..CHUNK].copy_from_slice(&p);
|
||||||
|
}
|
||||||
|
let mut paths = vec![];
|
||||||
|
for (i, d) in disks.iter_mut().enumerate() {
|
||||||
|
if i == 2 {
|
||||||
|
continue; // disco que morreu
|
||||||
|
}
|
||||||
|
let sb = md_super(n as u32, i as u32, data_off, (rows * CHUNK / 512) as u64);
|
||||||
|
d[4096..8192].copy_from_slice(&sb);
|
||||||
|
let p = dir.path().join(format!("disk{i}.img"));
|
||||||
|
std::fs::write(&p, &d).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(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let a = assemble_source(base, &asm).unwrap();
|
||||||
|
assert!(a.notes.iter().any(|n| n.contains("degradado")), "{:?}", a.notes);
|
||||||
|
assert_eq!(a.extra_parts.len(), 1);
|
||||||
|
assert_eq!(a.extra_parts[0].fs, FsKind::Ext);
|
||||||
|
let opts = ScanOptions { mode: ScanMode::Deep, extra_parts: a.extra_parts.clone(), covered: a.covered.clone(), ..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 == "ferias.dat").unwrap_or_else(|| panic!("{:?}", sink.found.iter().map(|f| &f.name).collect::<Vec<_>>()));
|
||||||
|
assert_eq!(f.size, 300_000);
|
||||||
|
let dest = tempfile::tempdir().unwrap();
|
||||||
|
let rep = recover::recover(a.source.as_ref(), &[f], dest.path(), &|| false, &mut |_, _, _| {}).unwrap();
|
||||||
|
assert!(rep.failed.is_empty(), "{:?}", rep.failed);
|
||||||
|
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,31 @@
|
|||||||
|
//! LVM2: imagem real do conjunto de testes do dfvfs (target/testdata/dfvfs/lvm.raw).
|
||||||
|
|
||||||
|
use guara_core::lvm;
|
||||||
|
use guara_core::partition::detect_fs;
|
||||||
|
use guara_core::source::{FileSource, Source};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn assembles_real_lvm_image() {
|
||||||
|
let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/testdata/dfvfs/lvm.raw");
|
||||||
|
if !p.exists() {
|
||||||
|
eprintln!("SKIP: execute scripts/fetch-testdata.sh");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let src: Arc<dyn Source> = Arc::new(FileSource::open(&p).unwrap());
|
||||||
|
let pv = lvm::read_pv(src.as_ref()).expect("PV");
|
||||||
|
assert_eq!(pv.vg_name, "test_volume_group");
|
||||||
|
let vgs = lvm::assemble_vgs(vec![(src, pv)]);
|
||||||
|
assert_eq!(vgs.len(), 1);
|
||||||
|
let vg = &vgs[0];
|
||||||
|
assert!(vg.warnings.is_empty(), "{:?}", vg.warnings);
|
||||||
|
let names: Vec<&str> = vg.lvs.iter().map(|(n, _)| n.as_str()).collect();
|
||||||
|
assert_eq!(names, ["test_logical_volume1", "test_logical_volume2"]);
|
||||||
|
for (n, lv) in &vg.lvs {
|
||||||
|
assert_eq!(lv.size(), 4 << 20, "{n}");
|
||||||
|
let fs = detect_fs(lv.as_ref(), 0);
|
||||||
|
eprintln!("{n}: {fs:?}");
|
||||||
|
// O segundo volume da imagem de referência foi criado e nunca formatado.
|
||||||
|
assert_eq!(fs.is_some(), n == "test_logical_volume1", "{n}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
//! RAID por software: arrays gerados por codificadores independentes (faixa a faixa),
|
||||||
|
//! lidos de volta inteiros, degradados e com superblocos md 1.x e 0.90.
|
||||||
|
|
||||||
|
use guara_core::raid::*;
|
||||||
|
use guara_core::source::{read_vec, MemSource, Source};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
const C: usize = 4096;
|
||||||
|
|
||||||
|
fn data(len: usize) -> Vec<u8> {
|
||||||
|
let mut x = 0x1234_5678_9ABC_DEF1u64;
|
||||||
|
(0..len)
|
||||||
|
.map(|_| {
|
||||||
|
x ^= x << 13;
|
||||||
|
x ^= x >> 7;
|
||||||
|
x ^= x << 17;
|
||||||
|
(x >> 24) as u8
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gmul(mut a: u8, mut b: u8) -> u8 {
|
||||||
|
let mut r = 0;
|
||||||
|
while b != 0 {
|
||||||
|
if b & 1 != 0 {
|
||||||
|
r ^= a;
|
||||||
|
}
|
||||||
|
let hi = a & 0x80;
|
||||||
|
a <<= 1;
|
||||||
|
if hi != 0 {
|
||||||
|
a ^= 0x1d;
|
||||||
|
}
|
||||||
|
b >>= 1;
|
||||||
|
}
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gpow(k: usize) -> u8 {
|
||||||
|
(0..k).fold(1, |a, _| gmul(a, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gera as imagens dos membros de um RAID 5/6 com a paridade na posição pedida.
|
||||||
|
fn encode_parity(level: Level, layout: Layout, n: usize, logical: &[u8]) -> Vec<Vec<u8>> {
|
||||||
|
let dd = if level == Level::R6 { n - 2 } else { n - 1 };
|
||||||
|
let rows = logical.len() / (C * dd);
|
||||||
|
let mut disks = vec![vec![0u8; rows * C]; n];
|
||||||
|
for row in 0..rows {
|
||||||
|
let r = row % n;
|
||||||
|
let (pd, qd) = match layout {
|
||||||
|
Layout::LeftSymmetric | Layout::LeftAsymmetric => {
|
||||||
|
let p = n - 1 - r;
|
||||||
|
(p, (p + 1) % n)
|
||||||
|
}
|
||||||
|
_ => (r, (r + 1) % n),
|
||||||
|
};
|
||||||
|
let sym = matches!(layout, Layout::LeftSymmetric | Layout::RightSymmetric);
|
||||||
|
// ordem dos discos de dados
|
||||||
|
let order: Vec<usize> = if sym {
|
||||||
|
let start = if level == Level::R6 { qd + 1 } else { pd + 1 };
|
||||||
|
(0..dd).map(|d| (start + d) % n).collect()
|
||||||
|
} else {
|
||||||
|
(0..n).filter(|&i| i != pd && !(level == Level::R6 && i == qd)).collect()
|
||||||
|
};
|
||||||
|
let mut p = vec![0u8; C];
|
||||||
|
let mut q = vec![0u8; C];
|
||||||
|
for (k, &disk) in order.iter().enumerate() {
|
||||||
|
let src = &logical[(row * dd + k) * C..(row * dd + k + 1) * C];
|
||||||
|
disks[disk][row * C..(row + 1) * C].copy_from_slice(src);
|
||||||
|
for i in 0..C {
|
||||||
|
p[i] ^= src[i];
|
||||||
|
q[i] ^= gmul(gpow(k), src[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disks[pd][row * C..(row + 1) * C].copy_from_slice(&p);
|
||||||
|
if level == Level::R6 {
|
||||||
|
disks[qd][row * C..(row + 1) * C].copy_from_slice(&q);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disks
|
||||||
|
}
|
||||||
|
|
||||||
|
fn members(imgs: &[Vec<u8>], missing: &[usize]) -> Vec<Option<Member>> {
|
||||||
|
imgs.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, v)| {
|
||||||
|
(!missing.contains(&i)).then(|| Member { src: Arc::new(MemSource(v.clone())), data_offset: 0 })
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check(level: Level, layout: Layout, n: usize, missing: &[usize]) {
|
||||||
|
let dd = if level == Level::R6 { n - 2 } else { n - 1 };
|
||||||
|
let logical = data(C * dd * 23);
|
||||||
|
let imgs = encode_parity(level, layout, n, &logical);
|
||||||
|
let r = RaidSource::new(RaidParams::simple(level, C as u64, layout, members(&imgs, missing))).unwrap();
|
||||||
|
assert_eq!(r.size(), logical.len() as u64);
|
||||||
|
let got = read_vec(&r, 0, logical.len()).unwrap();
|
||||||
|
assert!(got == logical, "{level:?} {layout:?} n={n} faltando {missing:?}");
|
||||||
|
// leitura desalinhada
|
||||||
|
let part = read_vec(&r, 5000, 30000).unwrap();
|
||||||
|
assert_eq!(part, &logical[5000..35000]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn raid5_all_layouts_intact_and_degraded() {
|
||||||
|
for layout in Layout::all_raid5() {
|
||||||
|
for n in [3, 4, 5] {
|
||||||
|
check(Level::R5, layout, n, &[]);
|
||||||
|
for m in 0..n {
|
||||||
|
check(Level::R5, layout, n, &[m]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn raid6_layouts_with_one_and_two_missing() {
|
||||||
|
for layout in Layout::all_raid5() {
|
||||||
|
for n in [4, 5, 6] {
|
||||||
|
check(Level::R6, layout, n, &[]);
|
||||||
|
for a in 0..n {
|
||||||
|
check(Level::R6, layout, n, &[a]);
|
||||||
|
for b in a + 1..n {
|
||||||
|
check(Level::R6, layout, n, &[a, b]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn raid0_raid1_linear_and_raid10_near() {
|
||||||
|
let logical = data(C * 24);
|
||||||
|
// RAID 0 com 3 discos
|
||||||
|
let mut imgs = vec![vec![]; 3];
|
||||||
|
for (c, ch) in logical.chunks(C).enumerate() {
|
||||||
|
imgs[c % 3].extend_from_slice(ch);
|
||||||
|
}
|
||||||
|
let r = RaidSource::new(RaidParams::simple(Level::R0, C as u64, Layout::LeftSymmetric, members(&imgs, &[]))).unwrap();
|
||||||
|
assert_eq!(read_vec(&r, 0, logical.len()).unwrap(), logical);
|
||||||
|
// RAID 1, um membro caído
|
||||||
|
let m = vec![logical.clone(), logical.clone()];
|
||||||
|
let r = RaidSource::new(RaidParams::simple(Level::R1, C as u64, Layout::LeftSymmetric, members(&m, &[0]))).unwrap();
|
||||||
|
assert_eq!(read_vec(&r, 0, logical.len()).unwrap(), logical);
|
||||||
|
// Linear
|
||||||
|
let (a, b) = logical.split_at(C * 10);
|
||||||
|
let r = RaidSource::new(RaidParams::simple(Level::Linear, C as u64, Layout::LeftSymmetric, members(&[a.to_vec(), b.to_vec()], &[]))).unwrap();
|
||||||
|
assert_eq!(read_vec(&r, 0, logical.len()).unwrap(), logical);
|
||||||
|
// RAID 10 near=2 com 4 discos: slots em sequência.
|
||||||
|
let n = 4;
|
||||||
|
let mut imgs = vec![vec![0u8; C * 12]; n];
|
||||||
|
for (c, ch) in logical.chunks(C).enumerate() {
|
||||||
|
for j in 0..2 {
|
||||||
|
let slot = c * 2 + j;
|
||||||
|
let (disk, row) = (slot % n, slot / n);
|
||||||
|
imgs[disk][row * C..(row + 1) * C].copy_from_slice(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut p = RaidParams::simple(Level::R10, C as u64, Layout::LeftSymmetric, members(&imgs, &[1]));
|
||||||
|
p.near = 2;
|
||||||
|
p.far = 1;
|
||||||
|
let r = RaidSource::new(p).unwrap();
|
||||||
|
assert_eq!(r.size(), (C * 24) as u64);
|
||||||
|
assert_eq!(read_vec(&r, 0, logical.len()).unwrap(), logical);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn md_v1_super(uuid: u8, level: u32, layout: u32, raid_disks: u32, dev_number: u32, role: u16, data_off_sectors: u64, data_sectors: u64, events: u64) -> Vec<u8> {
|
||||||
|
let mut b = vec![0u8; 4096];
|
||||||
|
b[0..4].copy_from_slice(&0xA92B_4EFCu32.to_le_bytes());
|
||||||
|
b[4..8].copy_from_slice(&1u32.to_le_bytes());
|
||||||
|
b[16..32].fill(uuid);
|
||||||
|
b[32..36].copy_from_slice(b"test");
|
||||||
|
b[72..76].copy_from_slice(&level.to_le_bytes());
|
||||||
|
b[76..80].copy_from_slice(&layout.to_le_bytes());
|
||||||
|
b[80..88].copy_from_slice(&data_sectors.to_le_bytes());
|
||||||
|
b[88..92].copy_from_slice(&((C / 512) as u32).to_le_bytes());
|
||||||
|
b[92..96].copy_from_slice(&raid_disks.to_le_bytes());
|
||||||
|
b[128..136].copy_from_slice(&data_off_sectors.to_le_bytes());
|
||||||
|
b[136..144].copy_from_slice(&data_sectors.to_le_bytes());
|
||||||
|
b[160..164].copy_from_slice(&dev_number.to_le_bytes());
|
||||||
|
b[200..208].copy_from_slice(&events.to_le_bytes());
|
||||||
|
b[220..224].copy_from_slice(&raid_disks.to_le_bytes());
|
||||||
|
for i in 0..raid_disks as usize {
|
||||||
|
b[256 + i * 2..258 + i * 2].copy_from_slice(&(i as u16).to_le_bytes());
|
||||||
|
}
|
||||||
|
let _ = role;
|
||||||
|
b
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn assembles_from_md_v12_superblocks_with_a_missing_disk() {
|
||||||
|
let n = 4;
|
||||||
|
let logical = data(C * 3 * 40);
|
||||||
|
let imgs = encode_parity(Level::R5, Layout::LeftSymmetric, n, &logical);
|
||||||
|
let data_off = 2048u64; // 1 MiB em setores
|
||||||
|
let mut members_in = vec![];
|
||||||
|
for (i, im) in imgs.iter().enumerate() {
|
||||||
|
if i == 2 {
|
||||||
|
continue; // disco perdido
|
||||||
|
}
|
||||||
|
let mut full = vec![0u8; 4096 + (data_off as usize) * 512 + im.len()];
|
||||||
|
let sb = md_v1_super(7, 5, 2, n as u32, i as u32, i as u16, data_off, (im.len() / 512) as u64, 100);
|
||||||
|
full[4096..8192].copy_from_slice(&sb);
|
||||||
|
full[4096 + data_off as usize * 512..][..im.len()].copy_from_slice(im);
|
||||||
|
// o superbloco 1.2 fica a 4 KiB; os dados começam em data_offset a partir do início
|
||||||
|
let mut real = vec![0u8; data_off as usize * 512 + im.len()];
|
||||||
|
real[4096..8192].copy_from_slice(&sb);
|
||||||
|
real[data_off as usize * 512..].copy_from_slice(im);
|
||||||
|
let src: Arc<dyn Source> = Arc::new(MemSource(real));
|
||||||
|
let sup = read_md_super(src.as_ref()).expect("superbloco");
|
||||||
|
assert_eq!(sup.version, "1.2");
|
||||||
|
assert_eq!(sup.role, Some(i as u32));
|
||||||
|
members_in.push((src, sup));
|
||||||
|
}
|
||||||
|
let arrays = assemble_md(members_in);
|
||||||
|
assert_eq!(arrays.len(), 1);
|
||||||
|
let a = arrays.into_iter().next().unwrap().unwrap();
|
||||||
|
assert_eq!(a.missing, 1);
|
||||||
|
assert_eq!(a.info.level, Level::R5);
|
||||||
|
assert_eq!(a.source.size(), logical.len() as u64);
|
||||||
|
assert!(read_vec(a.source.as_ref(), 0, logical.len()).unwrap() == logical);
|
||||||
|
}
|
||||||
@@ -12,4 +12,8 @@ for n in bitlk-images fvault2-images; do
|
|||||||
tar xJf "$tmp/$n.tar.xz" -C "$dest"
|
tar xJf "$tmp/$n.tar.xz" -C "$dest"
|
||||||
rm -rf "$tmp"
|
rm -rf "$tmp"
|
||||||
done
|
done
|
||||||
|
mkdir -p "$dest/dfvfs"
|
||||||
|
for f in lvm.raw apfs.raw apfs_encrypted.dmg hfsplus.raw hfs.raw xfs.raw xfs-bigtime.raw ext4.raw ext2.raw fvdetest.qcow2 luks1.raw luks2.raw bdetogo.raw; do
|
||||||
|
[ -f "$dest/dfvfs/$f" ] || curl -fsSL "https://raw.githubusercontent.com/log2timeline/dfvfs/main/test_data/$f" -o "$dest/dfvfs/$f"
|
||||||
|
done
|
||||||
echo "ok: $dest"
|
echo "ok: $dest"
|
||||||
Reference in new issue
Block a user