Files
guara/crates/guara-core/src/raid.rs
T
Lucas RayanandClaude Opus 5.5 beb83fef92
Release / Cria a release (push) Canceled after 0s
Release / CLI (linux-aarch64) (push) Canceled after 0s
Release / CLI (linux-x86_64) (push) Canceled after 0s
Release / CLI (macos-aarch64) (push) Canceled after 0s
Release / CLI (macos-x86_64) (push) Canceled after 0s
Release / CLI (windows-x86_64) (push) Canceled after 0s
Release / App desktop (linux-x86_64) (push) Canceled after 0s
Release / App desktop (macos-aarch64) (push) Canceled after 0s
Release / App desktop (macos-x86_64) (push) Canceled after 0s
Release / App desktop (windows-x86_64) (push) Canceled after 0s
Release / Publica a release (push) Canceled after 0s
Formatação (cargo fmt) e ajustes do clippy para o CI da 1.0.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:41:58 -03:00

760 lines
24 KiB
Rust

//! 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::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::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
}