use anyhow::{bail, Context, Result};
use nix::mount;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;
use libcoreinst::blockdev::*;
use libcoreinst::io::*;
use libcoreinst::util::*;
use libcoreinst::runcmd_output;
use crate::cmdline::*;
const PHYSICAL_ROOT_MOUNT: &str = "sysroot";
pub fn rootmap(config: RootmapConfig) -> Result<()> {
let root_mount_path = Path::new(&config.root_mount);
let rootfs_mount = Mount::from_existing(root_mount_path)?;
let physical_root_path = root_mount_path.join(PHYSICAL_ROOT_MOUNT);
let physical_mount = Mount::from_existing(physical_root_path)?;
let device = PathBuf::from(physical_mount.device());
let mut backing_devices = get_blkdev_deps_recursing(&device)?;
let on_multipath = is_on_multipath(&backing_devices)?;
backing_devices.push(device);
let mut kargs = Vec::new();
for backing_device in backing_devices {
if let Some(dev_kargs) = device_to_kargs(&rootfs_mount, backing_device)? {
kargs.extend(dev_kargs);
}
}
let root = if on_multipath {
format!(
"root=/dev/disk/by-uuid/dm-mpath-{}",
physical_mount.get_filesystem_uuid()?
)
} else {
format!("root=UUID={}", physical_mount.get_filesystem_uuid()?)
};
kargs.push(root);
kargs.push("rw".into());
let rootflags = runcmd_output!("coreos-rootflags", &config.root_mount)?;
let rootflags = rootflags.trim();
if !rootflags.is_empty() {
kargs.push(format!("rootflags={rootflags}"));
}
let boot_mount = get_boot_mount_from_cmdline_args(&config.boot_mount, &config.boot_device)?;
if let Some(mount) = boot_mount {
visit_bls_entry_options(mount.mountpoint(), |orig_options: &str| {
KargsEditor::new()
.append(&kargs)
.maybe_apply_to(orig_options)
})
.context("appending rootmap kargs")?;
eprintln!("Injected kernel arguments into BLS: {}", kargs.join(" "));
} else {
println!("{}", kargs.join(" "));
}
Ok(())
}
pub fn get_boot_mount_from_cmdline_args(
boot_mount: &Option<String>,
boot_device: &Option<String>,
) -> Result<Option<Mount>> {
if let Some(path) = boot_mount {
Ok(Some(Mount::from_existing(path)?))
} else if let Some(devpath) = boot_device {
let devinfo = lsblk_single(Path::new(devpath))?;
let fs = devinfo
.get("FSTYPE")
.with_context(|| format!("failed to query filesystem for {devpath}"))?;
Ok(Some(Mount::try_mount(
devpath,
fs,
mount::MsFlags::empty(),
)?))
} else {
Ok(None)
}
}
fn device_to_kargs(root: &Mount, device: PathBuf) -> Result<Option<Vec<String>>> {
let blkinfo = lsblk_single(&device)?;
let blktype = blkinfo
.get("TYPE")
.with_context(|| format!("missing TYPE for {}", device.display()))?;
if blktype.starts_with("raid") || blktype == "linear" {
Ok(Some(get_raid_kargs(&device)?))
} else if blktype == "crypt" {
if Disk::new(&device)?.is_luks_integrity()? {
Ok(None)
} else {
Ok(Some(get_luks_kargs(root, &device)?))
}
} else if blktype == "mpath" {
Ok(Some(get_multipath_kargs(&device)?))
} else if blktype == "part" || blktype == "disk" {
Ok(None)
} else {
bail!("unknown block device type {}", blktype)
}
}
fn get_raid_kargs(device: &Path) -> Result<Vec<String>> {
let details = mdadm_detail(device)?;
let uuid = details
.get("MD_UUID")
.with_context(|| format!("missing MD_UUID for {}", device.display()))?;
Ok(vec![format!("rd.md.uuid={uuid}")])
}
fn mdadm_detail(device: &Path) -> Result<HashMap<String, String>> {
let output = runcmd_output!("mdadm", "--detail", "--export", device)?;
output.lines().map(split_mdadm_line).collect()
}
fn split_mdadm_line(line: &str) -> Result<(String, String)> {
line.split_once('=')
.ok_or_else(|| anyhow::anyhow!("invalid mdadm line: {}", line))
.map(|(k, v)| (k.to_string(), v.to_string()))
}
fn get_luks_kargs(root: &Mount, device: &Path) -> Result<Vec<String>> {
let uuid = get_luks_uuid(device)?;
let name = get_luks_name(device)?;
let mut kargs = vec![format!("rd.luks.name={uuid}={name}")];
if crypttab_device_has_netdev(root, &name)? {
kargs.push("rd.neednet=1".into());
kargs.push("rd.luks.options=_netdev".into());
}
Ok(kargs)
}
fn crypttab_device_has_netdev(root: &Mount, dmname: &str) -> Result<bool> {
let crypttab_path = root.mountpoint().join("etc/crypttab");
let crypttab = std::fs::read_to_string(&crypttab_path)
.with_context(|| format!("opening {}", crypttab_path.display()))?;
for line in crypttab.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() < 2 {
bail!("crypttab line missing name or device: {}", line);
}
if fields[0] != dmname {
continue;
}
if fields.len() < 4 {
return Ok(false);
}
return Ok(fields[3].split(',').any(|opt| opt == "_netdev"));
}
bail!("couldn't find {} in {}", dmname, crypttab_path.display());
}
fn get_luks_name(device: &Path) -> Result<String> {
Ok(runcmd_output!(
"dmsetup",
"info",
"--columns",
"--noheadings",
"-o",
"name",
device
)?
.trim()
.into())
}
fn get_luks_uuid(device: &Path) -> Result<String> {
let deps = get_blkdev_deps(device)?;
match deps.as_slice() {
[] => bail!("missing parent device for {}", device.display()),
[device] => {
if Disk::new(device)?.is_luks_integrity()? {
return get_luks_uuid(device);
}
Ok(runcmd_output!("cryptsetup", "luksUUID", device)?
.trim()
.into())
}
_ => bail!(
"found multiple parent devices for crypt device {}",
device.display()
),
}
}
pub fn bind_boot(config: BindBootConfig) -> Result<()> {
let root_mount_path = Path::new(&config.root_mount);
let boot_mount = Mount::from_existing(&config.boot_mount)?;
let physical_root_path = root_mount_path.join(PHYSICAL_ROOT_MOUNT);
let root_mount = Mount::from_existing(physical_root_path)?;
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=") {
KargsEditor::new()
.append(&kargs)
.maybe_apply_to(orig_options)
} else {
Ok(None)
}
})
.context("appending boot kargs")?;
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()))?;
}
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();
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!("{root_uuid}\n"))
.with_context(|| format!("writing {}", root_uuid_stamp.display()))?;
}
#[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_colocated_esps(boot_mount.device())? {
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<P: AsRef<Path>>(uuid: &str, p: P) -> Result<()> {
let p = p.as_ref();
std::fs::write(p, format!("set BOOT_UUID=\"{uuid}\"\n"))
.with_context(|| format!("writing {}", p.display()))
}
fn is_on_multipath(backing_devices: &[PathBuf]) -> Result<bool> {
let blkinfo = match backing_devices.last() {
Some(p) => lsblk_single(p)?,
_ => return Ok(false),
};
Ok(blkinfo.get("TYPE").is_some_and(|t| t == "mpath"))
}
fn get_multipath_kargs(device: &Path) -> Result<Vec<String>> {
let mut kargs = Vec::new();
let canonical = std::fs::canonicalize(device)
.with_context(|| format!("failed to canonicalize device path: {}", device.display()))?;
let device_name = canonical
.file_name()
.and_then(|s| s.to_str())
.with_context(|| {
format!(
"failed to extract device name from canonical path: {}",
canonical.display()
)
})?;
let mpathd_output = runcmd_output!("multipathd", "show", "maps", "raw", "format", "%d %w")?;
let devices: HashMap<&str, &str> = mpathd_output
.lines()
.filter_map(|s| {
let mut parts = s.split_whitespace();
Some((parts.next()?, parts.next()?))
})
.collect();
let wwid = devices
.get(device_name)
.map(|w| w.to_string())
.with_context(|| {
format!(
"failed to find WWID in multipathd output for {}",
device_name
)
})?;
kargs.push(format!("mpath.wwid={}", wwid));
Ok(kargs)
}