diff --git a/src/kira_disk_layout.rs b/src/kira_disk_layout.rs index 363ce1d..672615a 100644 --- a/src/kira_disk_layout.rs +++ b/src/kira_disk_layout.rs @@ -18,9 +18,9 @@ Functions for working with disk layout */ +use crate::kira_size::KiraSize; use log; -use std::collections::HashMap; -use crate::kira_size::{KiraSize}; +use std::{collections::HashMap, io::Read, process::ExitStatus}; /// /// size - device size in bytes /// sector size - default 4096 @@ -38,6 +38,7 @@ impl std::fmt::Display for BlkDev { } impl BlkDev { + /// Create new BlkDev struct with specifyed name and size pub fn new(name: String, size: KiraSize) -> Self { Self { name: name, @@ -45,6 +46,9 @@ impl BlkDev { sector_size: KiraSize::new_b(4096), } } + + /// Create new BlkDev struct with name and size parsed from hashmap + /// that contains String key val pairs with "NAME" and "SIZE" keys pub fn from_hash_map(data: &HashMap) -> Option { Some(Self { name: data.get("NAME")?.clone(), @@ -52,7 +56,13 @@ impl BlkDev { sector_size: KiraSize::new_b(4096), }) } + /// returns dev name in a "/dev/name" form + pub fn full_name(&self) -> String { + format!("/dev/{}", self.name) + } + /// this is blocking function + /// Lists block devices in system thru "lsbk" command pub fn list_sys_blk_dev() -> Result, String> { use std::process::Command; @@ -139,6 +149,22 @@ impl FSType { _ => Self::UNKNOWN, } } + + /// return fs type that can be used with parted utility + pub fn to_parted_str(&self) -> &str { + match self { + Self::VFAT => "fat32", + Self::XFS => "xfs", + Self::LUKS => "luks", + Self::SWAP => "linux-swap", + Self::EXT4 => "ext4", + Self::BTRFS => "btrfs", + // workaroud as parted do not support bcachefs partition type + Self::BCACHEFS => "bcachefs", + // lets do something generic here instead of panic + Self::UNKNOWN => "linux-swap", + } + } } // fn part_type_to_color(t: &str) -> Color { @@ -177,13 +203,7 @@ pub struct PartInfo { impl std::fmt::Display for PartInfo { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{} {} {}", - &self.name, - &self.fs_type, - self.size - ) + write!(f, "{} {} {}", &self.name, &self.fs_type, self.size) } } @@ -291,7 +311,8 @@ pub fn align_part(size: u64, start: u64, align: u64) -> Option<(u64, u64, u64)> if align_size < align { log::warn!( "!!! align_size < align !!! align_size: {} align: {}", - align_size, align + align_size, + align ); return None; } @@ -316,7 +337,9 @@ impl PartLayout { pub fn read_from_disk(dev: &BlkDev) -> Result { let part_list = get_dev_part_blocking(&dev.name)?; - let parts_size = part_list.iter().fold(KiraSize::new_b(0), |acc, part| acc + part.size); + let parts_size = part_list + .iter() + .fold(KiraSize::new_b(0), |acc, part| acc + part.size); Ok(Self { dev: dev.clone(), empty_space: dev.size - parts_size, @@ -392,7 +415,7 @@ impl PartLayout { ) .add_part_reserve( swap_size, - FSType::XFS, + FSType::BTRFS, Some("root".into()), None, Some("/".into()), @@ -415,6 +438,195 @@ impl PartLayout { } } +/// Executes parted command with given arguments +fn exec_parted(args: &Vec<&str>) -> Result { + use std::process::Command; + + match Command::new("parted").args(args).output() { + Err(ex) => Err(ex.to_string()), + Ok(out) => { + if out.status.success() { + String::from_utf8(out.stdout).map_err(|ex| ex.to_string()) + } else { + Err(out.status.to_string()) + } + } + } +} + +fn parted_get_last_part_num(dev: &BlkDev) -> Result { + // parted -m /dev/loop0 print + match exec_parted(&vec!["-m", dev.full_name().as_str(), "print"]) { + Ok(res) => { + match res.lines().last().and_then(|last_line| { + last_line + .split_once(':') + .and_then(|(first_part, a)| u32::from_str_radix(first_part, 10).ok()) + }) { + Some(n) => Ok(n), + None => Err("Error parsing parted output".to_string()), + } + } + Err(ex) => Err(ex), + } +} + +/// init gpt partition table +/// parted --script $install_device mklabel gpt +/// Its blocking function +pub fn parted_init_gpt_part_table(dev: &BlkDev) -> Result<(), String> { + exec_parted(&vec![ + "--script", + dev.full_name().as_str(), + "mklabel", + "gpt", + ]) + .map(|_| ()) +} + +const LINUX_ROOT_X86_64_TYPE: &str = "4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709"; + +/// creating partition from PartInfo specification +/// It assumes that we add partitions sequentially to empty space (for part attributes settings) +/// Otherwise can produce bonkers layout +pub fn parted_mkpart(dev: &BlkDev, part: &PartInfo) -> Result<(), String> { + let part_end = part.start + part.size; + + // gpt label should be in double qutes + let gpt_label = if let Some(s) = &part.gpt_label { + format!("\"{}\"", s) + } else { + "\"\"".to_string() + }; + // creating partition + exec_parted(&vec![ + "--script", + dev.full_name().as_str(), + "mkpart", + gpt_label.as_str(), + part.fs_type.to_parted_str(), + part.start.to_parted_bytes_str().as_str(), + part_end.to_parted_bytes_str().as_str(), + ])?; + + // set partition options depending on its role + if let Some(role) = &part.role { + // getting part number of last created partition (hopefully) + let part_id = parted_get_last_part_num(dev)?; + match role { + PartRole::EFI => { + exec_parted(&vec![ + "--script", + dev.full_name().as_str(), + "set", + &part_id.to_string(), + "esp", + "on", + ])?; + } + // parted --script $install_device type 3 4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709 + PartRole::ROOT => { + exec_parted(&vec![ + "--script", + dev.full_name().as_str(), + "type", + &part_id.to_string(), + LINUX_ROOT_X86_64_TYPE, + ])?; + } + _ => (), + } + }; + + Ok(()) +} + +/// creating single drive bcachefs volume +pub fn bcachefs_format( + dev: &BlkDev, + compression: Option, + password: Option, +) -> Result<(), String> { + // sudo bcachefs format --compression=lz4 --background_compression=lz4 --encrypted /dev/loop0 + // --data_checksum=none - do not make sense on single dev volumes + // sudo bcachefs format --force --compression=lz4 --background_compression=lz4 --encrypted --passphrase_file=./test.pass /dev/loop0 + use std::fs; + use std::process::Command; + + let mut arg = vec!["format", "--force", "--data_checksum=none"]; + // if compression algorythm specified + if let Some(compresss_alg) = &compression { + arg.push(compresss_alg.as_str()); + } + // if we want encrypted volume + if let Some(passw) = password { + // creating tmp file with password + fs::write("/tmp/bzzpsspass.txt", &passw).map_err(|ex| ex.to_string())?; + arg.push("--encrypted"); + arg.push("--passphrase_file=/tmp/bzzpsspass.txt"); + } + + // add dev name at the end + let dev_name = dev.full_name(); + arg.push(dev_name.as_str()); + + let out = Command::new("bcachefs") + .args(arg) + .output() + .map_err(|ex| ex.to_string())?; + + // ignore any errors while deleting pass tmp file + let _ = fs::remove_file("/tmp/bzzpsspass.txt"); + + if out.status.success() { + Ok(()) + } else { + Err(String::from_utf8(out.stderr).map_err(|ex| ex.to_string())?) + } +} + +/// unlok encrypted bcache volume +pub fn bcachefs_unlock(dev: &BlkDev, pass: String) -> Result<(), String> { + use std::fs; + use std::process::Command; + + // bcachefs unlock --file=/tmp/bzzpsspass.txt /dev/loop0 + let dev_name = dev.full_name(); + let arg = vec!["unlock", "--file=/tmp/bzzpsspass.txt ", dev_name.as_str()]; + // creating tmp file with password + fs::write("/tmp/bzzpsspass.txt", &pass).map_err(|ex| ex.to_string())?; + + let out = Command::new("bcachefs") + .args(arg) + .output() + .map_err(|ex| ex.to_string())?; + // ignore any errors while deleting pass tmp file + let _ = fs::remove_file("/tmp/bzzpsspass.txt"); + + if out.status.success() { + Ok(()) + } else { + Err(String::from_utf8(out.stderr).map_err(|ex| ex.to_string())?) + } +} + + +/// create subvolume +pub fn bcachefs_create_subvolume(vol_path: &str) -> Result<(), String> { + use std::process::Command; + + // sudo bcachefs subvolume create /mnt/test/nyan + let out = Command::new("bcachefs") + .args(["subvolume", "create", vol_path]) + .output() + .map_err(|ex| ex.to_string())?; + if out.status.success() { + Ok(()) + } else { + Err(String::from_utf8(out.stderr).map_err(|ex| ex.to_string())?) + } +} + #[cfg(test)] mod tests { use rand::RngExt; @@ -466,4 +678,3 @@ mod tests { } } } -