diff --git a/Cargo.lock b/Cargo.lock index a68cb12d1..87c386e53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2361,6 +2361,7 @@ name = "system-reinstall-bootc" version = "0.1.9" dependencies = [ "anyhow", + "bootc-mount", "bootc-utils", "clap", "crossterm 0.29.0", diff --git a/mount/src/mount.rs b/mount/src/mount.rs index 112489f3b..86a09f26d 100644 --- a/mount/src/mount.rs +++ b/mount/src/mount.rs @@ -45,29 +45,28 @@ pub struct Filesystem { pub children: Option>, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Default)] pub struct Findmnt { pub filesystems: Vec, } -fn run_findmnt(args: &[&str], path: &str) -> Result { - let o: Findmnt = Command::new("findmnt") - .args([ - "-J", - "-v", - // If you change this you probably also want to change the Filesystem struct above - "--output=SOURCE,TARGET,MAJ:MIN,FSTYPE,OPTIONS,UUID", - ]) - .args(args) - .arg(path) - .log_debug() - .run_and_parse_json()?; +pub fn run_findmnt(args: &[&str], path: Option<&str>) -> Result { + let mut cmd = Command::new("findmnt"); + cmd.args([ + "-J", + "-v", + // If you change this you probably also want to change the Filesystem struct above + "--output=SOURCE,TARGET,MAJ:MIN,FSTYPE,OPTIONS,UUID", + ]) + .args(args) + .args(path); + let o: Findmnt = cmd.log_debug().run_and_parse_json()?; Ok(o) } // Retrieve a mounted filesystem from a device given a matching path fn findmnt_filesystem(args: &[&str], path: &str) -> Result { - let o = run_findmnt(args, path)?; + let o = run_findmnt(args, Some(path))?; o.filesystems .into_iter() .next() @@ -90,7 +89,7 @@ pub fn inspect_filesystem_by_uuid(uuid: &str) -> Result { // Check if a specified device contains an already mounted filesystem // in the root mount namespace pub fn is_mounted_in_pid1_mountns(path: &str) -> Result { - let o = run_findmnt(&["-N"], "1")?; + let o = run_findmnt(&["-N"], Some("1"))?; let mounted = o.filesystems.iter().any(|fs| is_source_mounted(path, fs)); diff --git a/system-reinstall-bootc/Cargo.toml b/system-reinstall-bootc/Cargo.toml index 759da0de6..251b0cbb0 100644 --- a/system-reinstall-bootc/Cargo.toml +++ b/system-reinstall-bootc/Cargo.toml @@ -16,6 +16,7 @@ platforms = ["*-unknown-linux-gnu"] [dependencies] anyhow = { workspace = true } +bootc-mount = { path = "../mount" } bootc-utils = { path = "../utils" } clap = { workspace = true, features = ["derive"] } crossterm = "0.29.0" diff --git a/system-reinstall-bootc/src/btrfs.rs b/system-reinstall-bootc/src/btrfs.rs new file mode 100644 index 000000000..57e08bb18 --- /dev/null +++ b/system-reinstall-bootc/src/btrfs.rs @@ -0,0 +1,27 @@ +use anyhow::Result; +use bootc_mount::Filesystem; + +pub(crate) fn check_root_siblings() -> Result> { + let mounts = bootc_mount::run_findmnt(&[], None)?; + let problem_filesystems: Vec = mounts + .filesystems + .iter() + .filter(|fs| fs.target == "/") + .flat_map(|root| { + let children: Vec<&Filesystem> = root + .children + .iter() + .flatten() + .filter(|child| child.source == root.source) + .collect(); + children + }) + .map(|zs| { + format!( + "Type: {}, Mount Point: {}, Source: {}", + zs.fstype, zs.target, zs.source + ) + }) + .collect(); + Ok(problem_filesystems) +} diff --git a/system-reinstall-bootc/src/lvm.rs b/system-reinstall-bootc/src/lvm.rs new file mode 100644 index 000000000..facf9e289 --- /dev/null +++ b/system-reinstall-bootc/src/lvm.rs @@ -0,0 +1,84 @@ +use std::process::Command; + +use anyhow::Result; +use bootc_mount::run_findmnt; +use bootc_utils::CommandRunExt; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub(crate) struct Lvs { + report: Vec, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct LvsReport { + lv: Vec, +} + +#[derive(Debug, Deserialize, Clone)] +pub(crate) struct LogicalVolume { + lv_name: String, + lv_size: String, + lv_path: String, + vg_name: String, +} + +pub(crate) fn parse_volumes(group: Option<&str>) -> Result> { + if which::which("podman").is_err() { + tracing::debug!("lvs binary not found. Skipping logical volume check."); + return Ok(Vec::::new()); + } + + let mut cmd = Command::new("lvs"); + cmd.args([ + "--reportformat=json", + "-o", + "lv_name,lv_size,lv_path,vg_name", + ]) + .args(group); + + let output: Lvs = cmd.run_and_parse_json()?; + + Ok(output + .report + .iter() + .flat_map(|r| r.lv.iter().cloned()) + .collect()) +} + +pub(crate) fn check_root_siblings() -> Result> { + let all_volumes = parse_volumes(None)?; + + // first look for a lv mounted to '/' + // then gather all the sibling lvs in the vg along with their mount points + let siblings: Vec = all_volumes + .iter() + .filter(|lv| { + let mount = run_findmnt(&["-S", &lv.lv_path], None).unwrap_or_default(); + if let Some(fs) = mount.filesystems.first() { + &fs.target == "/" + } else { + false + } + }) + .flat_map(|root_lv| parse_volumes(Some(root_lv.vg_name.as_str())).unwrap_or_default()) + .try_fold(Vec::new(), |mut acc, r| -> anyhow::Result<_> { + let mount = run_findmnt(&["-S", &r.lv_path], None)?; + let mount_path = if let Some(fs) = mount.filesystems.first() { + &fs.target + } else { + "" + }; + + if mount_path != "/" { + acc.push(format!( + "Type: LVM, Mount Point: {}, LV: {}, VG: {}, Size: {}", + mount_path, r.lv_name, r.vg_name, r.lv_size + )) + }; + + Ok(acc) + })?; + + Ok(siblings) +} diff --git a/system-reinstall-bootc/src/main.rs b/system-reinstall-bootc/src/main.rs index 51868aae0..feffb8c0c 100644 --- a/system-reinstall-bootc/src/main.rs +++ b/system-reinstall-bootc/src/main.rs @@ -4,7 +4,9 @@ use anyhow::{ensure, Context, Result}; use bootc_utils::CommandRunExt; use rustix::process::getuid; +mod btrfs; mod config; +mod lvm; mod podman; mod prompt; pub(crate) mod users; @@ -40,6 +42,8 @@ fn run() -> Result<()> { prompt::get_ssh_keys(ssh_key_file_path)?; + prompt::mount_warning()?; + let mut reinstall_podman_command = podman::reinstall_command(&config.bootc_image, ssh_key_file_path); @@ -48,6 +52,9 @@ fn run() -> Result<()> { println!(); println!("{}", reinstall_podman_command.to_string_pretty()); + println!(); + println!("After reboot, the current root will be available in the /sysroot directory. Existing mounts will not be automatically mounted by the bootc system unless they are defined in the bootc image. Some automatic cleanup of the previous root will be performed."); + prompt::temporary_developer_protection_prompt()?; reinstall_podman_command diff --git a/system-reinstall-bootc/src/prompt.rs b/system-reinstall-bootc/src/prompt.rs index 10a9e7dc6..19f6bc7a8 100644 --- a/system-reinstall-bootc/src/prompt.rs +++ b/system-reinstall-bootc/src/prompt.rs @@ -1,4 +1,4 @@ -use crate::{prompt, users::get_all_users_keys}; +use crate::{btrfs, lvm, prompt, users::get_all_users_keys}; use anyhow::{ensure, Context, Result}; use crossterm::event::{self, Event}; @@ -92,6 +92,34 @@ pub(crate) fn ask_yes_no(prompt: &str, default: bool) -> Result { .context("prompting") } +pub(crate) fn press_enter() { + println!(); + println!("Press to continue."); + + loop { + if let Event::Key(_) = event::read().unwrap() { + break; + } + } +} + +pub(crate) fn mount_warning() -> Result<()> { + let mut mounts = btrfs::check_root_siblings()?; + mounts.extend(lvm::check_root_siblings()?); + + if !mounts.is_empty() { + println!(); + println!("NOTICE: the following mounts are left unchanged by this tool and will not be automatically mounted unless specified in the bootc image. Consult the bootc documentation to determine the appropriate action for your system."); + println!(); + for m in mounts { + println!("{m}"); + } + press_enter(); + } + + Ok(()) +} + /// Gather authorized keys for all user's of the host system /// prompt the user to select which users's keys will be imported /// into the target system's root user's authorized_keys file