-
Notifications
You must be signed in to change notification settings - Fork 131
Move composefs setup root to bootc initramfs #1550
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,15 @@ | ||
//! Code for bootc that goes into the initramfs. | ||
//! At the current time, this is mostly just a no-op. | ||
// SPDX-License-Identifier: Apache-2.0 OR MIT | ||
|
||
mod mount; | ||
|
||
use anyhow::Result; | ||
|
||
fn setup_root() -> Result<()> { | ||
let _ = std::fs::metadata("/sysroot/usr")?; | ||
println!("setup OK"); | ||
Ok(()) | ||
} | ||
use clap::Parser; | ||
use mount::{gpt_workaround, setup_root, Args}; | ||
|
||
fn main() -> Result<()> { | ||
let v = std::env::args().collect::<Vec<_>>(); | ||
let args = match v.as_slice() { | ||
[] => anyhow::bail!("Missing argument".to_string()), | ||
[_, rest @ ..] => rest, | ||
}; | ||
match args { | ||
[] => anyhow::bail!("Missing argument".to_string()), | ||
[s] if s == "setup-root" => setup_root(), | ||
[o, ..] => anyhow::bail!(format!("Unknown command {o}")), | ||
} | ||
let args = Args::parse(); | ||
gpt_workaround()?; | ||
setup_root(args) | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,294 @@ | ||||||
//! Mount helpers for bootc-initramfs | ||||||
|
||||||
use std::{ | ||||||
ffi::OsString, | ||||||
fmt::Debug, | ||||||
io::ErrorKind, | ||||||
os::fd::{AsFd, OwnedFd}, | ||||||
path::{Path, PathBuf}, | ||||||
}; | ||||||
|
||||||
use anyhow::{Context, Result}; | ||||||
use clap::Parser; | ||||||
use rustix::{ | ||||||
fs::{major, minor, mkdirat, openat, stat, symlink, Mode, OFlags, CWD}, | ||||||
io::Errno, | ||||||
mount::{ | ||||||
fsconfig_create, fsconfig_set_string, fsmount, open_tree, unmount, FsMountFlags, | ||||||
MountAttrFlags, OpenTreeFlags, UnmountFlags, | ||||||
}, | ||||||
}; | ||||||
use serde::Deserialize; | ||||||
|
||||||
use composefs::{ | ||||||
fsverity::{FsVerityHashValue, Sha256HashValue}, | ||||||
mount::{mount_at, FsHandle}, | ||||||
mountcompat::{overlayfs_set_fd, overlayfs_set_lower_and_data_fds, prepare_mount}, | ||||||
repository::Repository, | ||||||
}; | ||||||
use composefs_boot::cmdline::get_cmdline_composefs; | ||||||
|
||||||
// Config file | ||||||
#[derive(Clone, Copy, Debug, Deserialize)] | ||||||
#[serde(rename_all = "lowercase")] | ||||||
enum MountType { | ||||||
None, | ||||||
Bind, | ||||||
Overlay, | ||||||
Transient, | ||||||
} | ||||||
|
||||||
#[derive(Debug, Default, Deserialize)] | ||||||
struct RootConfig { | ||||||
#[serde(default)] | ||||||
transient: bool, | ||||||
} | ||||||
|
||||||
#[derive(Debug, Default, Deserialize)] | ||||||
struct MountConfig { | ||||||
mount: Option<MountType>, | ||||||
#[serde(default)] | ||||||
transient: bool, | ||||||
} | ||||||
|
||||||
#[derive(Deserialize, Default)] | ||||||
struct Config { | ||||||
#[serde(default)] | ||||||
etc: MountConfig, | ||||||
#[serde(default)] | ||||||
var: MountConfig, | ||||||
#[serde(default)] | ||||||
root: RootConfig, | ||||||
} | ||||||
|
||||||
/// Command-line arguments | ||||||
#[derive(Parser, Debug)] | ||||||
#[command(version)] | ||||||
pub struct Args { | ||||||
#[arg(help = "Execute this command (for testing)")] | ||||||
/// Execute this command (for testing) | ||||||
pub cmd: Vec<OsString>, | ||||||
|
||||||
#[arg( | ||||||
long, | ||||||
default_value = "/sysroot", | ||||||
help = "sysroot directory in initramfs" | ||||||
)] | ||||||
/// sysroot directory in initramfs | ||||||
pub sysroot: PathBuf, | ||||||
|
||||||
#[arg( | ||||||
long, | ||||||
default_value = "/usr/lib/composefs/setup-root-conf.toml", | ||||||
help = "Config path (for testing)" | ||||||
)] | ||||||
/// Config path (for testing) | ||||||
pub config: PathBuf, | ||||||
|
||||||
// we want to test in a userns, but can't mount erofs there | ||||||
#[arg(long, help = "Bind mount root-fs from (for testing)")] | ||||||
/// Bind mount root-fs from (for testing) | ||||||
pub root_fs: Option<PathBuf>, | ||||||
|
||||||
#[arg(long, help = "Kernel commandline args (for testing)")] | ||||||
/// Kernel commandline args (for testing) | ||||||
pub cmdline: Option<String>, | ||||||
|
||||||
#[arg(long, help = "Mountpoint (don't replace sysroot, for testing)")] | ||||||
/// Mountpoint (don't replace sysroot, for testing) | ||||||
pub target: Option<PathBuf>, | ||||||
} | ||||||
|
||||||
// Helpers | ||||||
fn open_dir(dirfd: impl AsFd, name: impl AsRef<Path> + Debug) -> rustix::io::Result<OwnedFd> { | ||||||
openat( | ||||||
dirfd, | ||||||
name.as_ref(), | ||||||
OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC, | ||||||
Mode::empty(), | ||||||
) | ||||||
.inspect_err(|_| { | ||||||
eprintln!("Failed to open dir {name:?}"); | ||||||
}) | ||||||
} | ||||||
|
||||||
fn ensure_dir(dirfd: impl AsFd, name: &str) -> rustix::io::Result<OwnedFd> { | ||||||
match mkdirat(dirfd.as_fd(), name, 0o700.into()) { | ||||||
Ok(()) | Err(Errno::EXIST) => {} | ||||||
Err(err) => Err(err)?, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The use of
Suggested change
|
||||||
} | ||||||
open_dir(dirfd, name) | ||||||
} | ||||||
|
||||||
fn bind_mount(fd: impl AsFd, path: &str) -> rustix::io::Result<OwnedFd> { | ||||||
open_tree( | ||||||
fd.as_fd(), | ||||||
path, | ||||||
OpenTreeFlags::OPEN_TREE_CLONE | ||||||
| OpenTreeFlags::OPEN_TREE_CLOEXEC | ||||||
| OpenTreeFlags::AT_EMPTY_PATH, | ||||||
) | ||||||
.inspect_err(|_| { | ||||||
eprintln!("Open tree failed for {path}"); | ||||||
}) | ||||||
} | ||||||
|
||||||
fn mount_tmpfs() -> Result<OwnedFd> { | ||||||
let tmpfs = FsHandle::open("tmpfs")?; | ||||||
fsconfig_create(tmpfs.as_fd())?; | ||||||
Ok(fsmount( | ||||||
tmpfs.as_fd(), | ||||||
FsMountFlags::FSMOUNT_CLOEXEC, | ||||||
MountAttrFlags::empty(), | ||||||
)?) | ||||||
} | ||||||
|
||||||
fn overlay_state(base: impl AsFd, state: impl AsFd, source: &str) -> Result<()> { | ||||||
let upper = ensure_dir(state.as_fd(), "upper")?; | ||||||
let work = ensure_dir(state.as_fd(), "work")?; | ||||||
|
||||||
let overlayfs = FsHandle::open("overlay")?; | ||||||
fsconfig_set_string(overlayfs.as_fd(), "source", source)?; | ||||||
overlayfs_set_fd(overlayfs.as_fd(), "workdir", work.as_fd())?; | ||||||
overlayfs_set_fd(overlayfs.as_fd(), "upperdir", upper.as_fd())?; | ||||||
overlayfs_set_lower_and_data_fds(&overlayfs, base.as_fd(), None::<OwnedFd>)?; | ||||||
fsconfig_create(overlayfs.as_fd())?; | ||||||
let fs = fsmount( | ||||||
overlayfs.as_fd(), | ||||||
FsMountFlags::FSMOUNT_CLOEXEC, | ||||||
MountAttrFlags::empty(), | ||||||
)?; | ||||||
|
||||||
Ok(mount_at(fs, base, ".")?) | ||||||
} | ||||||
|
||||||
fn overlay_transient(base: impl AsFd) -> Result<()> { | ||||||
overlay_state(base, prepare_mount(mount_tmpfs()?)?, "transient") | ||||||
} | ||||||
|
||||||
fn open_root_fs(path: &Path) -> Result<OwnedFd> { | ||||||
let rootfs = open_tree( | ||||||
CWD, | ||||||
path, | ||||||
OpenTreeFlags::OPEN_TREE_CLONE | OpenTreeFlags::OPEN_TREE_CLOEXEC, | ||||||
)?; | ||||||
|
||||||
// https://github.com/bytecodealliance/rustix/issues/975 | ||||||
// mount_setattr(rootfs.as_fd()), ..., { ... MountAttrFlags::MOUNT_ATTR_RDONLY ... }, ...)?; | ||||||
|
||||||
Ok(rootfs) | ||||||
} | ||||||
|
||||||
/// Prepares a floating mount for composefs and returns the fd | ||||||
/// | ||||||
/// # Arguments | ||||||
/// * sysroot - fd for /sysroot | ||||||
/// * name - Name of the EROFS image to be mounted | ||||||
/// * insecure - Whether fsverity is optional or not | ||||||
pub fn mount_composefs_image(sysroot: &OwnedFd, name: &str, insecure: bool) -> Result<OwnedFd> { | ||||||
let mut repo = Repository::<Sha256HashValue>::open_path(sysroot, "composefs")?; | ||||||
repo.set_insecure(insecure); | ||||||
repo.mount(name).context("Failed to mount composefs image") | ||||||
} | ||||||
|
||||||
fn mount_subdir( | ||||||
new_root: impl AsFd, | ||||||
state: impl AsFd, | ||||||
subdir: &str, | ||||||
config: MountConfig, | ||||||
default: MountType, | ||||||
) -> Result<()> { | ||||||
let mount_type = match config.mount { | ||||||
Some(mt) => mt, | ||||||
None => match config.transient { | ||||||
true => MountType::Transient, | ||||||
false => default, | ||||||
}, | ||||||
}; | ||||||
Comment on lines
+201
to
+207
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
|
||||||
match mount_type { | ||||||
MountType::None => Ok(()), | ||||||
MountType::Bind => Ok(mount_at(bind_mount(&state, subdir)?, &new_root, subdir)?), | ||||||
MountType::Overlay => overlay_state( | ||||||
open_dir(&new_root, subdir)?, | ||||||
open_dir(&state, subdir)?, | ||||||
"overlay", | ||||||
), | ||||||
MountType::Transient => overlay_transient(open_dir(&new_root, subdir)?), | ||||||
} | ||||||
} | ||||||
|
||||||
pub(crate) fn gpt_workaround() -> Result<()> { | ||||||
// https://github.com/systemd/systemd/issues/35017 | ||||||
let rootdev = stat("/dev/gpt-auto-root"); | ||||||
|
||||||
let rootdev = match rootdev { | ||||||
Ok(r) => r, | ||||||
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(()), | ||||||
Err(e) => Err(e)?, | ||||||
}; | ||||||
|
||||||
let target = format!( | ||||||
"/dev/block/{}:{}", | ||||||
major(rootdev.st_rdev), | ||||||
minor(rootdev.st_rdev) | ||||||
); | ||||||
symlink(target, "/run/systemd/volatile-root")?; | ||||||
Ok(()) | ||||||
} | ||||||
|
||||||
/// Sets up /sysroot for switch-root | ||||||
pub fn setup_root(args: Args) -> Result<()> { | ||||||
let config = match std::fs::read_to_string(args.config) { | ||||||
Ok(text) => toml::from_str(&text)?, | ||||||
Err(err) if err.kind() == ErrorKind::NotFound => Config::default(), | ||||||
Err(err) => Err(err)?, | ||||||
}; | ||||||
|
||||||
let sysroot = open_dir(CWD, &args.sysroot) | ||||||
.with_context(|| format!("Failed to open sysroot {:?}", args.sysroot))?; | ||||||
|
||||||
let cmdline = match &args.cmdline { | ||||||
Some(cmdline) => cmdline, | ||||||
// TODO: Deduplicate this with composefs branch karg parser | ||||||
None => &std::fs::read_to_string("/proc/cmdline")?, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a new problem per se but this raises the now-more-obvious issue that we added a kargs parser into bootc, and not into composefs-boot... Except though of course the "detect composefs" karg parsing is only on the composefs branch... Let's just add a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought about using the new parser, but it's in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For now, I've just added the TODO comment There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah +1 to moving the cmdline parser to bootc-utils, I'll go do that now There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Feels slightly large for Or, slicing this differently...a general thing that's weird now is that But we could redo things so that there's e.g.:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 I'll ask claude nicely to redo it into its own crate. I'll probably call it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ➡️ #1554 |
||||||
}; | ||||||
let (image, insecure) = get_cmdline_composefs::<Sha256HashValue>(cmdline)?; | ||||||
|
||||||
let new_root = match args.root_fs { | ||||||
Some(path) => open_root_fs(&path).context("Failed to clone specified root fs")?, | ||||||
None => mount_composefs_image(&sysroot, &image.to_hex(), insecure)?, | ||||||
}; | ||||||
|
||||||
// we need to clone this before the next step to make sure we get the old one | ||||||
let sysroot_clone = bind_mount(&sysroot, "")?; | ||||||
|
||||||
// Ideally we build the new root filesystem together before we mount it, but that only works on | ||||||
// 6.15 and later. Before 6.15 we can't mount into a floating tree, so mount it first. This | ||||||
// will leave an abandoned clone of the sysroot mounted under it, but that's OK for now. | ||||||
if cfg!(feature = "pre-6.15") { | ||||||
mount_at(&new_root, CWD, &args.sysroot)?; | ||||||
} | ||||||
|
||||||
if config.root.transient { | ||||||
overlay_transient(&new_root)?; | ||||||
} | ||||||
|
||||||
match mount_at(&sysroot_clone, &new_root, "sysroot") { | ||||||
Ok(()) | Err(Errno::NOENT) => {} | ||||||
Err(err) => Err(err)?, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BTW with stuff like this I personally am fine one marks it resolved without changing anything |
||||||
} | ||||||
|
||||||
// etc + var | ||||||
let state = open_dir(open_dir(&sysroot, "state/deploy")?, image.to_hex())?; | ||||||
mount_subdir(&new_root, &state, "etc", config.etc, MountType::Overlay)?; | ||||||
mount_subdir(&new_root, &state, "var", config.var, MountType::Bind)?; | ||||||
|
||||||
if cfg!(not(feature = "pre-6.15")) { | ||||||
// Replace the /sysroot with the new composed root filesystem | ||||||
unmount(&args.sysroot, UnmountFlags::DETACH)?; | ||||||
mount_at(&new_root, CWD, &args.sysroot)?; | ||||||
} | ||||||
|
||||||
Ok(()) | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The command-line argument
target
is defined here but it appears to be unused in thesetup_root
function. The logic insetup_root
always usesargs.sysroot
as the mount destination. Iftarget
is intended for testing purposes to provide an alternative mount point, it should be utilized in the mounting logic.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well spotted!