diff --git a/src/bin/rdcore/cmdline.rs b/src/bin/rdcore/cmdline.rs index 97793ded9..fbd0f7fee 100644 --- a/src/bin/rdcore/cmdline.rs +++ b/src/bin/rdcore/cmdline.rs @@ -30,6 +30,8 @@ use structopt::StructOpt; pub enum Cmd { /// Generate rootmap kargs and optionally inject into BLS configs Rootmap(RootmapConfig), + /// Generate bootmap kargs and binds bootfs to rootfs and GRUB + BindBoot(BindBootConfig), /// Modify kargs in BLS configs Kargs(KargsConfig), /// Copy data from stdin to stdout, checking piecewise hashes @@ -56,6 +58,16 @@ pub struct RootmapConfig { pub root_mount: String, } +#[derive(Debug, StructOpt)] +pub struct BindBootConfig { + /// Path to rootfs mount + #[structopt(value_name = "ROOT_MOUNT")] + pub root_mount: String, + /// Path to bootfs mount + #[structopt(value_name = "BOOT_MOUNT")] + pub boot_mount: String, +} + #[derive(Debug, StructOpt)] pub struct KargsConfig { // see comment block in rootmap command above diff --git a/src/bin/rdcore/main.rs b/src/bin/rdcore/main.rs index 12f3ffc9d..49f13e98b 100644 --- a/src/bin/rdcore/main.rs +++ b/src/bin/rdcore/main.rs @@ -27,6 +27,7 @@ fn main() -> Result<()> { match Cmd::from_args() { Cmd::Kargs(c) => kargs::kargs(&c), Cmd::Rootmap(c) => rootmap::rootmap(&c), + Cmd::BindBoot(c) => rootmap::bind_boot(&c), Cmd::StreamHash(c) => stream_hash::stream_hash(&c), Cmd::VerifyUniqueFsLabel(c) => unique_fs::verify_unique_fs(&c), } diff --git a/src/bin/rdcore/rootmap.rs b/src/bin/rdcore/rootmap.rs index e93bef7f4..6e2d0d774 100644 --- a/src/bin/rdcore/rootmap.rs +++ b/src/bin/rdcore/rootmap.rs @@ -218,3 +218,73 @@ fn get_luks_uuid(device: &Path) -> Result { .trim() .into()) } + +pub fn bind_boot(config: &BindBootConfig) -> Result<()> { + let boot_mount = Mount::from_existing(&config.boot_mount)?; + let root_mount = Mount::from_existing(&config.root_mount)?; + let boot_uuid = boot_mount.get_filesystem_uuid()?; + let root_uuid = root_mount.get_filesystem_uuid()?; + + let kargs = vec![format!("boot=UUID={}", boot_uuid)]; + let changed = visit_bls_entry_options(boot_mount.mountpoint(), |orig_options: &str| { + if !orig_options.starts_with("boot=") && !orig_options.contains(" boot=") { + bls_entry_options_delete_and_append_kargs(orig_options, &[], &kargs, &[]) + } else { + // boot= karg already exists; let's not add anything + Ok(None) + } + }) + .context("appending boot kargs")?; + + // put it in /run also for the first boot real root mount + // https://github.com/coreos/fedora-coreos-config/blob/8661649009/overlay.d/05core/usr/lib/systemd/system-generators/coreos-boot-mount-generator#L105-L108 + if changed { + let boot_uuid_run = Path::new("/run/coreos/bootfs_uuid"); + let parent = boot_uuid_run.parent().unwrap(); + std::fs::create_dir_all(parent) + .with_context(|| format!("creating {}", parent.display()))?; + std::fs::write(boot_uuid_run, format!("{}\n", &boot_uuid)) + .with_context(|| format!("writing {}", boot_uuid_run.display()))?; + } + + // bind rootfs to bootfs + let root_uuid_stamp = boot_mount.mountpoint().join(".root_uuid"); + if root_uuid_stamp.exists() { + let bound_root_uuid = std::fs::read_to_string(&root_uuid_stamp) + .with_context(|| format!("reading {}", root_uuid_stamp.display()))?; + let bound_root_uuid = bound_root_uuid.trim(); + // Let it slide if it already matches the rootfs... that shouldn't happen unless the user + // is trying to force a rerun of Ignition. In that case, we'll have nicer errors and + // warnings elsewhere. + if bound_root_uuid != root_uuid { + bail!( + "boot filesystem already bound to a root filesystem (UUID: {})", + bound_root_uuid + ); + } + } else { + std::fs::write(&root_uuid_stamp, format!("{}\n", root_uuid)) + .with_context(|| format!("writing {}", root_uuid_stamp.display()))?; + } + + // now bind GRUB to bootfs + #[cfg(not(target_arch = "s390x"))] + { + let grub_bios_path = boot_mount.mountpoint().join("grub2/bootuuid.cfg"); + write_boot_uuid_grub2_dropin(&boot_uuid, grub_bios_path)?; + } + + for esp in find_esps()? { + let mount = Mount::try_mount(&esp, "vfat", mount::MsFlags::empty())?; + let vendor_dir = find_efi_vendor_dir(&mount)?; + let grub_efi_path = vendor_dir.join("bootuuid.cfg"); + write_boot_uuid_grub2_dropin(&boot_uuid, grub_efi_path)?; + } + Ok(()) +} + +fn write_boot_uuid_grub2_dropin>(uuid: &str, p: P) -> Result<()> { + let p = p.as_ref(); + std::fs::write(p, format!("set BOOT_UUID=\"{}\"\n", uuid)) + .with_context(|| format!("writing {}", p.display())) +} diff --git a/src/blockdev.rs b/src/blockdev.rs index cda58c2b5..4727f7c5e 100644 --- a/src/blockdev.rs +++ b/src/blockdev.rs @@ -877,6 +877,57 @@ pub fn lsblk(dev: &Path, with_deps: bool) -> Result> Ok(result) } +pub fn find_esps() -> Result> { + const ESP_TYPE_GUID: &str = "c12a7328-f81f-11d2-ba4b-00a0c93ec93b"; + let mut cmd = Command::new("lsblk"); + // Older lsblk, e.g. in CentOS 7.6, doesn't support PATH, but --paths option + cmd.arg("--pairs") + .arg("--paths") + .arg("--output") + .arg("NAME,PARTTYPE"); + let output = cmd_output(&mut cmd)?; + output + .lines() + .filter_map(|line| { + let dev = split_lsblk_line(line); + if dev.get("PARTTYPE").map(|t| t.as_str()) == Some(ESP_TYPE_GUID) { + Some( + dev.get("NAME") + .cloned() + .ok_or_else(|| anyhow!("ESP device with missing NAME")), + ) + } else { + None + } + }) + .collect() +} + +/// This is basically a Rust version of: +/// https://github.com/coreos/coreos-assembler/blob/d3c7ec094a02/src/cmd-buildextend-live#L492-L495 +pub fn find_efi_vendor_dir(efi_mount: &Mount) -> Result { + let p = efi_mount.mountpoint().join("EFI"); + let mut vendor_dir: Vec = Vec::new(); + for ent in p.read_dir()? { + let ent = ent.with_context(|| format!("reading directory entry in {}", p.display()))?; + if ent.file_name() == "BOOT" { + continue; + } + if !ent.file_type()?.is_dir() { + continue; + } + vendor_dir.push(ent.path()); + } + if vendor_dir.len() != 1 { + bail!( + "Expected one vendor dir on {}, got {}", + efi_mount.device(), + vendor_dir.len() + ); + } + Ok(vendor_dir.pop().unwrap()) +} + /// Parse key-value pairs from lsblk --pairs. /// Newer versions of lsblk support JSON but the one in CentOS 7 doesn't. fn split_lsblk_line(line: &str) -> HashMap { diff --git a/src/install.rs b/src/install.rs index 59f852b21..95f00f0ed 100644 --- a/src/install.rs +++ b/src/install.rs @@ -607,6 +607,8 @@ fn bls_entry_options_write_platform(orig_options: &str, platform: &str) -> Resul /// /// Note that on s390x, this does not handle the call to `zipl`. We expect it to be done at a /// higher level if needed for batching purposes. +/// +/// Returns `true` if BLS content was modified. pub fn visit_bls_entry( mountpoint: &Path, f: impl Fn(&str) -> Result>, @@ -675,7 +677,7 @@ pub fn visit_bls_entry( /// Wrapper around `visit_bls_entry` to specifically visit just the BLS entry's `options` line and /// optionally update it if the function returns new content. Errors out if none or more than one -/// `options` field was found. +/// `options` field was found. Returns `true` if BLS content was modified. pub fn visit_bls_entry_options( mountpoint: &Path, f: impl Fn(&str) -> Result>,