Skip to content

Commit 923b0ed

Browse files
cgwaltersomertuc
authored andcommitted
WIP: Add support for --replace-mode=alongside for ostree target
Ironically our support for `--replace-mode=alongside` breaks when we're targeting an already extant ostree host, because when we first blow away the `/boot` directory, this means the ostree stack loses its knowledge that we're in a booted deployment, and will attempt to GC it... ostreedev/ostree-rs-ext@8fa019b is a key part of the fix for that. However, a notable improvement we can do here is to grow this whole thing into a real "factory reset" mode, and this will be a compelling answer to coreos/fedora-coreos-tracker#399 To implement this though we need to support configuring the stateroot and not just hardcode `default`. Signed-off-by: Omer Tuchfeld <omer@tuchfeld.dev>
1 parent 9d55f15 commit 923b0ed

File tree

3 files changed

+125
-19
lines changed

3 files changed

+125
-19
lines changed

lib/src/install.rs

Lines changed: 78 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -564,26 +564,34 @@ pub(crate) fn print_configuration() -> Result<()> {
564564
}
565565

566566
#[context("Creating ostree deployment")]
567-
async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result<Storage> {
567+
async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result<(Storage, bool)> {
568568
let sepolicy = state.load_policy()?;
569569
let sepolicy = sepolicy.as_ref();
570570
// Load a fd for the mounted target physical root
571571
let rootfs_dir = &root_setup.rootfs_fd;
572572
let rootfs = root_setup.rootfs.as_path();
573573
let cancellable = gio::Cancellable::NONE;
574574

575+
let stateroot = state.stateroot();
576+
577+
let has_ostree = rootfs_dir.try_exists("ostree/repo")?;
578+
if !has_ostree {
579+
Task::new_and_run(
580+
"Initializing ostree layout",
581+
"ostree",
582+
["admin", "init-fs", "--modern", rootfs.as_str()],
583+
)?;
584+
} else {
585+
println!("Reusing extant ostree layout");
586+
let path = ".".into();
587+
let _ = crate::utils::open_dir_remount_rw(rootfs_dir, path)
588+
.context("remounting sysroot as read-write")?;
589+
}
590+
575591
// Ensure that the physical root is labeled.
576592
// Another implementation: https://github.com/coreos/coreos-assembler/blob/3cd3307904593b3a131b81567b13a4d0b6fe7c90/src/create_disk.sh#L295
577593
crate::lsm::ensure_dir_labeled(rootfs_dir, "", Some("/".into()), 0o755.into(), sepolicy)?;
578594

579-
let stateroot = state.stateroot();
580-
581-
Task::new_and_run(
582-
"Initializing ostree layout",
583-
"ostree",
584-
["admin", "init-fs", "--modern", rootfs.as_str()],
585-
)?;
586-
587595
// And also label /boot AKA xbootldr, if it exists
588596
let bootdir = rootfs.join("boot");
589597
if bootdir.try_exists()? {
@@ -607,9 +615,14 @@ async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result
607615
let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(rootfs)));
608616
sysroot.load(cancellable)?;
609617

610-
sysroot
611-
.init_osname(stateroot, cancellable)
612-
.context("initializing stateroot")?;
618+
let stateroot_exists = rootfs_dir.try_exists(format!("ostree/deploy/{stateroot}"))?;
619+
if stateroot_exists {
620+
anyhow::bail!("Cannot redeploy over extant stateroot {stateroot}");
621+
} else {
622+
sysroot
623+
.init_osname(stateroot, cancellable)
624+
.context("initializing stateroot")?;
625+
}
613626

614627
let sysroot_dir = Dir::reopen_dir(&crate::utils::sysroot_fd(&sysroot))?;
615628

@@ -637,14 +650,15 @@ async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result
637650
let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(rootfs)));
638651
sysroot.load(cancellable)?;
639652
let sysroot = SysrootLock::new_from_sysroot(&sysroot).await?;
640-
Storage::new(sysroot, &temp_run)
653+
Ok((Storage::new(sysroot, &temp_run)?, has_ostree))
641654
}
642655

643656
#[context("Creating ostree deployment")]
644657
async fn install_container(
645658
state: &State,
646659
root_setup: &RootSetup,
647660
sysroot: &ostree::Sysroot,
661+
has_ostree: bool,
648662
) -> Result<(ostree::Deployment, InstallAleph)> {
649663
let sepolicy = state.load_policy()?;
650664
let sepolicy = sepolicy.as_ref();
@@ -730,6 +744,7 @@ async fn install_container(
730744
options.kargs = Some(kargs.as_slice());
731745
options.target_imgref = Some(&state.target_imgref);
732746
options.proxy_cfg = proxy_cfg;
747+
options.no_clean = has_ostree;
733748
let imgstate = crate::utils::async_task_with_spinner(
734749
"Deploying container image",
735750
ostree_container::deploy::deploy(&sysroot, stateroot, &src_imageref, Some(options)),
@@ -1272,10 +1287,11 @@ async fn install_with_sysroot(
12721287
sysroot: &Storage,
12731288
boot_uuid: &str,
12741289
bound_images: &[crate::boundimage::ResolvedBoundImage],
1290+
has_ostree: bool,
12751291
) -> Result<()> {
12761292
// And actually set up the container in that root, returning a deployment and
12771293
// the aleph state (see below).
1278-
let (_deployment, aleph) = install_container(state, rootfs, &sysroot).await?;
1294+
let (_deployment, aleph) = install_container(state, rootfs, &sysroot, has_ostree).await?;
12791295
// Write the aleph data that captures the system state at the time of provisioning for aid in future debugging.
12801296
rootfs
12811297
.rootfs_fd
@@ -1336,6 +1352,19 @@ async fn install_to_filesystem_impl(state: &State, rootfs: &mut RootSetup) -> Re
13361352
.ok_or_else(|| anyhow!("No uuid for boot/root"))?;
13371353
tracing::debug!("boot uuid={boot_uuid}");
13381354

1355+
// If we're doing an alongside install, then the /dev bootupd sees needs to be the host's.
1356+
// What we probably really want to do here is tunnel in the host's /dev properly, but for now
1357+
// just copy /dev/disk
1358+
if rootfs.skip_finalize {
1359+
if !Utf8Path::new("/dev/disk").try_exists()? {
1360+
Task::new_and_run(
1361+
"Copying host /dev/disk",
1362+
"cp",
1363+
["-a", "/proc/1/root/dev/disk", "/dev/disk"],
1364+
)?;
1365+
}
1366+
}
1367+
13391368
let bound_images = if state.config_opts.skip_bound_images {
13401369
Vec::new()
13411370
} else {
@@ -1356,8 +1385,16 @@ async fn install_to_filesystem_impl(state: &State, rootfs: &mut RootSetup) -> Re
13561385

13571386
// Initialize the ostree sysroot (repo, stateroot, etc.)
13581387
{
1359-
let sysroot = initialize_ostree_root(state, rootfs).await?;
1360-
install_with_sysroot(state, rootfs, &sysroot, &boot_uuid, &bound_images).await?;
1388+
let (sysroot, has_ostree) = initialize_ostree_root(state, rootfs).await?;
1389+
install_with_sysroot(
1390+
state,
1391+
rootfs,
1392+
&sysroot,
1393+
&boot_uuid,
1394+
&bound_images,
1395+
has_ostree,
1396+
)
1397+
.await?;
13611398
// We must drop the sysroot here in order to close any open file
13621399
// descriptors.
13631400
}
@@ -1498,7 +1535,8 @@ fn remove_all_in_dir_no_xdev(d: &Dir) -> Result<()> {
14981535

14991536
#[context("Removing boot directory content")]
15001537
fn clean_boot_directories(rootfs: &Dir) -> Result<()> {
1501-
let bootdir = rootfs.open_dir(BOOT).context("Opening /boot")?;
1538+
let bootdir =
1539+
crate::utils::open_dir_remount_rw(rootfs, BOOT.into()).context("Opening /boot")?;
15021540
// This should not remove /boot/efi note.
15031541
remove_all_in_dir_no_xdev(&bootdir)?;
15041542
if ARCH_USES_EFI {
@@ -1589,12 +1627,35 @@ pub(crate) async fn install_to_filesystem(
15891627
if !st.is_dir() {
15901628
anyhow::bail!("Not a directory: {root_path}");
15911629
}
1630+
1631+
let inspect = crate::mount::inspect_filesystem(&fsopts.root_path)?;
1632+
1633+
let alternative_root = fsopts.root_path.join("sysroot");
1634+
let root_path = match inspect.source.as_str() {
1635+
// Our target filesystem is an overlay, the true root is in `/sysroot`
1636+
"overlay" => {
1637+
tracing::debug!(
1638+
"Overlay filesystem detected, using {alternative_root} instead of {root_path} as target root"
1639+
);
1640+
&alternative_root
1641+
}
1642+
_ => root_path,
1643+
};
15921644
let rootfs_fd = Dir::open_ambient_dir(root_path, cap_std::ambient_authority())
15931645
.with_context(|| format!("Opening target root directory {root_path}"))?;
1646+
1647+
tracing::debug!("Root filesystem: {root_path}");
1648+
15941649
if let Some(false) = ostree_ext::mountutil::is_mountpoint(&rootfs_fd, ".")? {
15951650
anyhow::bail!("Not a mountpoint: {root_path}");
15961651
}
15971652

1653+
let fsopts = {
1654+
let mut fsopts = fsopts.clone();
1655+
fsopts.root_path = root_path.clone();
1656+
fsopts
1657+
};
1658+
15981659
// Gather global state, destructuring the provided options.
15991660
// IMPORTANT: We might re-execute the current process in this function (for SELinux among other things)
16001661
// IMPORTANT: and hence anything that is done before MUST BE IDEMPOTENT.

lib/src/lsm.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,6 @@ pub(crate) fn ensure_dir_labeled_recurse(
333333
Ok(())
334334
}
335335

336-
/// A wrapper for creating a directory, also optionally setting a SELinux label.
337336
#[cfg(feature = "install")]
338337
pub(crate) fn ensure_dir_labeled(
339338
root: &Dir,
@@ -365,6 +364,11 @@ pub(crate) fn ensure_dir_labeled(
365364

366365
root.ensure_dir_with(local_destname, &DirBuilder::new())
367366
.with_context(|| format!("Opening {local_destname}"))?;
367+
// tracing::trace!("Rooted at {}", root.canonicalize()
368+
tracing::trace!(
369+
"Going to open {} for fchmod",
370+
local_destname.as_std_path().to_str().unwrap()
371+
);
368372
let dirfd = cap_std_ext::cap_primitives::fs::open(
369373
&root.as_filelike_view(),
370374
local_destname.as_std_path(),

lib/src/utils.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ use std::process::Command;
55
use std::time::Duration;
66

77
use anyhow::{Context, Result};
8-
use cap_std_ext::cap_std::fs::Dir;
8+
use camino::Utf8Path;
9+
use cap_std_ext::{cap_std::fs::Dir, prelude::CapStdExtCommandExt};
10+
use fn_error_context::context;
911
use ostree::glib;
1012
use ostree_ext::container::SignatureSource;
1113
use ostree_ext::ostree;
14+
use std::os::fd::AsFd;
1215

1316
/// Try to look for keys injected by e.g. rpm-ostree requesting machine-local
1417
/// changes; if any are present, return `true`.
@@ -55,6 +58,44 @@ pub(crate) fn find_mount_option<'a>(
5558
.next()
5659
}
5760

61+
/// Try to (heuristically) determine if the provided path is a mount root.
62+
pub(crate) fn is_mountpoint(root: &Dir, path: &Utf8Path) -> Result<Option<bool>> {
63+
// https://github.com/systemd/systemd/blob/8fbf0a214e2fe474655b17a4b663122943b55db0/src/basic/mountpoint-util.c#L176
64+
use rustix::fs::{AtFlags, StatxFlags};
65+
66+
// SAFETY(unwrap): We can infallibly convert an i32 into a u64.
67+
let mountroot_flag: u64 = libc::STATX_ATTR_MOUNT_ROOT.try_into().unwrap();
68+
match rustix::fs::statx(
69+
root.as_fd(),
70+
path.as_std_path(),
71+
AtFlags::NO_AUTOMOUNT | AtFlags::SYMLINK_NOFOLLOW,
72+
StatxFlags::empty(),
73+
) {
74+
Ok(r) => {
75+
let present = (r.stx_attributes_mask & mountroot_flag) > 0;
76+
Ok(present.then(|| r.stx_attributes & mountroot_flag > 0))
77+
}
78+
Err(e) if e == rustix::io::Errno::NOSYS => Ok(None),
79+
Err(e) => Err(e.into()),
80+
}
81+
}
82+
83+
/// Given a target directory, if it's a read-only mount, then remount it writable
84+
#[context("Opening {target} with writable mount")]
85+
pub(crate) fn open_dir_remount_rw(root: &Dir, target: &Utf8Path) -> Result<Dir> {
86+
if is_mountpoint(root, target)?.unwrap_or_default() {
87+
tracing::debug!("Target {target} is a mountpoint, remounting rw");
88+
let st = Command::new("mount")
89+
.args(["-o", "remount,rw", target.as_str()])
90+
.cwd_dir(root.try_clone()?)
91+
.status()?;
92+
if !st.success() {
93+
anyhow::bail!("Failed to remount: {st:?}");
94+
}
95+
}
96+
root.open_dir(target).map_err(anyhow::Error::new)
97+
}
98+
5899
pub(crate) fn spawn_editor(tmpf: &tempfile::NamedTempFile) -> Result<()> {
59100
let editor_variables = ["EDITOR"];
60101
// These roughly match https://github.com/systemd/systemd/blob/769ca9ab557b19ee9fb5c5106995506cace4c68f/src/shared/edit-util.c#L275

0 commit comments

Comments
 (0)