From 2082741b0a0e1c772bc6a6492dc23d785a5fd047 Mon Sep 17 00:00:00 2001 From: ckyrouac Date: Tue, 6 May 2025 10:29:22 -0400 Subject: [PATCH 1/2] mount: Make path optional in run_findmnt This allows retrieving all mounts. Signed-off-by: ckyrouac --- mount/src/mount.rs | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/mount/src/mount.rs b/mount/src/mount.rs index 112489f3b..bc9e671bb 100644 --- a/mount/src/mount.rs +++ b/mount/src/mount.rs @@ -50,24 +50,23 @@ 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)); From 118dcedf2299d1872b5e25961062c95c1acac894 Mon Sep 17 00:00:00 2001 From: ckyrouac Date: Tue, 6 May 2025 10:31:03 -0400 Subject: [PATCH 2/2] reinstall: Add warnings about mounts This uses findmnt to locate filesystem mounts that are on the same source as the root mount. If any are found, the user is warned these filesystems will persist unmounted in the bootc system. The user must hit to proceed. This does the same for logical volumes in the same group as root. It also adds a generic warning to help the user understand what will happen after rebooting into the bootc system. Signed-off-by: ckyrouac --- Cargo.lock | 1 + mount/src/mount.rs | 2 +- system-reinstall-bootc/Cargo.toml | 1 + system-reinstall-bootc/src/btrfs.rs | 27 +++++++++ system-reinstall-bootc/src/lvm.rs | 84 ++++++++++++++++++++++++++++ system-reinstall-bootc/src/main.rs | 7 +++ system-reinstall-bootc/src/prompt.rs | 30 +++++++++- 7 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 system-reinstall-bootc/src/btrfs.rs create mode 100644 system-reinstall-bootc/src/lvm.rs 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 bc9e671bb..86a09f26d 100644 --- a/mount/src/mount.rs +++ b/mount/src/mount.rs @@ -45,7 +45,7 @@ pub struct Filesystem { pub children: Option>, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Default)] pub struct Findmnt { pub filesystems: Vec, } 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