From 5b48d079e7469cbe7c6c86a1fd649e6b272b1606 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Fri, 8 Mar 2024 16:32:40 -0500 Subject: [PATCH] install: Support gathering more info for host root (including LVM) Teach `install to-existing-root` how to gather kernel arguments and information we need from `/proc/cmdline` (such as `rd.lvm.lv`). In this case too, we need to adjust the args to use `-v /dev:/dev` because we need the stuff udev generates from the real host root. With these things together, I can do a bootc alongside install targeting a Fedora Server instance. Closes: https://github.com/containers/bootc/issues/175 Signed-off-by: Colin Walters --- docs/src/bootc-install.md | 2 +- lib/src/cli.rs | 4 +- lib/src/install.rs | 107 +++++++++++++++++++++++++++++++++----- lib/src/kernel.rs | 46 ++++++++++++++++ lib/src/lib.rs | 2 + 5 files changed, 146 insertions(+), 15 deletions(-) create mode 100644 lib/src/kernel.rs diff --git a/docs/src/bootc-install.md b/docs/src/bootc-install.md index 482a28d5..9ba96fb7 100644 --- a/docs/src/bootc-install.md +++ b/docs/src/bootc-install.md @@ -238,7 +238,7 @@ support the root storage setup already initialized. The core command should look like this (root/elevated permission required): ```bash -podman run --rm --privileged -v /var/lib/containers:/var/lib/containers -v /:/target \ +podman run --rm --privileged -v /dev:/dev -v /var/lib/containers:/var/lib/containers -v /:/target \ --pid=host --security-opt label=type:unconfined_t \ \ bootc install to-existing-root diff --git a/lib/src/cli.rs b/lib/src/cli.rs index 18e58cf2..cb14abfc 100644 --- a/lib/src/cli.rs +++ b/lib/src/cli.rs @@ -533,7 +533,9 @@ async fn run_from_opt(opt: Opt) -> Result<()> { #[cfg(feature = "install")] Opt::Install(opts) => match opts { InstallOpts::ToDisk(opts) => crate::install::install_to_disk(opts).await, - InstallOpts::ToFilesystem(opts) => crate::install::install_to_filesystem(opts).await, + InstallOpts::ToFilesystem(opts) => { + crate::install::install_to_filesystem(opts, false).await + } InstallOpts::ToExistingRoot(opts) => { crate::install::install_to_existing_root(opts).await } diff --git a/lib/src/install.rs b/lib/src/install.rs index e2287914..9d2ca858 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -40,6 +40,7 @@ use serde::{Deserialize, Serialize}; use self::baseline::InstallBlockDeviceOpts; use crate::containerenv::ContainerExecutionInfo; +use crate::mount::Filesystem; use crate::task::Task; use crate::utils::sigpolicy_from_opts; @@ -1223,9 +1224,46 @@ fn clean_boot_directories(rootfs: &Dir) -> Result<()> { Ok(()) } +struct RootMountInfo { + mount_spec: String, + kargs: Vec, +} + +/// Discover how to mount the root filesystem, using existing kernel arguments and information +/// about the root mount. +fn find_root_args_to_inherit(cmdline: &[&str], root_info: &Filesystem) -> Result { + let cmdline = || cmdline.iter().map(|&s| s); + let root = crate::kernel::find_first_cmdline_arg(cmdline(), "root"); + // If we have a root= karg, then use that + let (mount_spec, kargs) = if let Some(root) = root { + let rootflags = cmdline().find(|arg| arg.starts_with(crate::kernel::ROOTFLAGS)); + let inherit_kargs = + cmdline().filter(|arg| arg.starts_with(crate::kernel::INITRD_ARG_PREFIX)); + ( + root.to_owned(), + rootflags + .into_iter() + .chain(inherit_kargs) + .map(ToOwned::to_owned) + .collect(), + ) + } else { + let uuid = root_info + .uuid + .as_deref() + .ok_or_else(|| anyhow!("No filesystem uuid found in target root"))?; + (format!("UUID={uuid}"), Vec::new()) + }; + + Ok(RootMountInfo { mount_spec, kargs }) +} + /// Implementation of the `bootc install to-filsystem` CLI command. #[context("Installing to filesystem")] -pub(crate) async fn install_to_filesystem(opts: InstallToFilesystemOpts) -> Result<()> { +pub(crate) async fn install_to_filesystem( + opts: InstallToFilesystemOpts, + targeting_host_root: bool, +) -> Result<()> { let fsopts = opts.filesystem_opts; let root_path = &fsopts.root_path; @@ -1266,25 +1304,39 @@ pub(crate) async fn install_to_filesystem(opts: InstallToFilesystemOpts) -> Resu // We support overriding the mount specification for root (i.e. LABEL vs UUID versus // raw paths). - let (root_mount_spec, root_extra) = if let Some(s) = fsopts.root_mount_spec { - (s, None) + let root_info = if let Some(s) = fsopts.root_mount_spec { + RootMountInfo { + mount_spec: s.to_string(), + kargs: Vec::new(), + } + } else if targeting_host_root { + // In the to-existing-root case, look at /proc/cmdline + let cmdline = crate::kernel::parse_cmdline()?; + let cmdline = cmdline.iter().map(|s| s.as_str()).collect::>(); + find_root_args_to_inherit(&cmdline, &inspect)? } else { + // Otherwise, gather metadata from the provided root and use its provided UUID as a + // default root= karg. let uuid = inspect .uuid .as_deref() .ok_or_else(|| anyhow!("No filesystem uuid found in target root"))?; - let uuid = format!("UUID={uuid}"); - tracing::debug!("root {uuid}"); - let opts = match inspect.fstype.as_str() { + let kargs = match inspect.fstype.as_str() { "btrfs" => { let subvol = crate::utils::find_mount_option(&inspect.options, "subvol"); - subvol.map(|vol| format!("rootflags=subvol={vol}")) + subvol + .map(|vol| format!("rootflags=subvol={vol}")) + .into_iter() + .collect::>() } - _ => None, + _ => Vec::new(), }; - (uuid, opts) + RootMountInfo { + mount_spec: format!("UUID={uuid}"), + kargs, + } }; - tracing::debug!("Root mount spec: {root_mount_spec}"); + tracing::debug!("Root mount: {} {:?}", root_info.mount_spec, root_info.kargs); let boot_is_mount = { let root_dev = rootfs_fd.dir_metadata()?.dev(); @@ -1333,7 +1385,7 @@ pub(crate) async fn install_to_filesystem(opts: InstallToFilesystemOpts) -> Resu }; tracing::debug!("Backing device: {backing_device}"); - let rootarg = format!("root={root_mount_spec}"); + let rootarg = format!("root={}", root_info.mount_spec); let mut boot = if let Some(spec) = fsopts.boot_mount_spec { Some(MountSpec::new(&spec, "/boot")) } else { @@ -1351,7 +1403,7 @@ pub(crate) async fn install_to_filesystem(opts: InstallToFilesystemOpts) -> Resu let bootarg = boot.as_ref().map(|boot| format!("boot={}", &boot.source)); let kargs = [rootarg] .into_iter() - .chain(root_extra) + .chain(root_info.kargs) .chain([RW_KARG.to_string()]) .chain(bootarg) .collect::>(); @@ -1390,7 +1442,7 @@ pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) -> config_opts: opts.config_opts, }; - install_to_filesystem(opts).await + install_to_filesystem(opts, true).await } #[test] @@ -1411,3 +1463,32 @@ fn test_mountspec() { ms.push_option("relatime"); assert_eq!(ms.to_fstab(), "/dev/vda4 /boot auto ro,relatime 0 0"); } + +#[test] +fn test_gather_root_args() { + // A basic filesystem using a UUID + let inspect = Filesystem { + source: "/dev/vda4".into(), + fstype: "xfs".into(), + options: "rw".into(), + uuid: Some("965eb3c7-5a3f-470d-aaa2-1bcf04334bc6".into()), + }; + let r = find_root_args_to_inherit(&[], &inspect).unwrap(); + assert_eq!(r.mount_spec, "UUID=965eb3c7-5a3f-470d-aaa2-1bcf04334bc6"); + + // In this case we take the root= from the kernel cmdline + let r = find_root_args_to_inherit( + &[ + "root=/dev/mapper/root", + "rw", + "someother=karg", + "rd.lvm.lv=root", + "systemd.debug=1", + ], + &inspect, + ) + .unwrap(); + assert_eq!(r.mount_spec, "/dev/mapper/root"); + assert_eq!(r.kargs.len(), 1); + assert_eq!(r.kargs[0], "rd.lvm.lv=root"); +} diff --git a/lib/src/kernel.rs b/lib/src/kernel.rs new file mode 100644 index 00000000..ecc0994c --- /dev/null +++ b/lib/src/kernel.rs @@ -0,0 +1,46 @@ +use anyhow::Result; +use fn_error_context::context; + +/// This is used by dracut. +pub(crate) const INITRD_ARG_PREFIX: &str = "rd."; +/// The kernel argument for configuring the rootfs flags. +pub(crate) const ROOTFLAGS: &str = "rootflags="; + +/// Parse the kernel command line. This is strictly +/// speaking not a correct parser, as the Linux kernel +/// supports quotes. However, we don't yet need that here. +/// +/// See systemd's code for one userspace parser. +#[context("Reading /proc/cmdline")] +pub(crate) fn parse_cmdline() -> Result> { + let cmdline = std::fs::read_to_string("/proc/cmdline")?; + let r = cmdline + .split_ascii_whitespace() + .map(ToOwned::to_owned) + .collect(); + Ok(r) +} + +/// Return the value for the string in the vector which has the form target_key=value +pub(crate) fn find_first_cmdline_arg<'a>( + args: impl Iterator, + target_key: &str, +) -> Option<&'a str> { + args.filter_map(|arg| { + if let Some((k, v)) = arg.split_once('=') { + if target_key == k { + return Some(v); + } + } + None + }) + .next() +} + +#[test] +fn test_find_first() { + let kargs = &["foo=bar", "root=/dev/vda", "blah", "root=/dev/other"]; + let kargs = || kargs.iter().map(|&s| s); + assert_eq!(find_first_cmdline_arg(kargs(), "root"), Some("/dev/vda")); + assert_eq!(find_first_cmdline_arg(kargs(), "nonexistent"), None); +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index aad5d95b..5b416f71 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -41,6 +41,8 @@ mod containerenv; mod install; mod k8sapitypes; #[cfg(feature = "install")] +mod kernel; +#[cfg(feature = "install")] pub(crate) mod mount; #[cfg(feature = "install")] mod podman;