Skip to content

Commit 4c3f543

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 <[email protected]>
1 parent 4621d76 commit 4c3f543

File tree

5 files changed

+101
-22
lines changed

5 files changed

+101
-22
lines changed

Cargo.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ lto = "yes"
2020
[workspace.dependencies]
2121
anyhow = "1.0.82"
2222
camino = "1.1.6"
23-
cap-std-ext = "4.0.2"
23+
cap-std-ext = "4.0.3"
2424
chrono = { version = "0.4.38", default-features = false }
2525
clap = "4.5.4"
2626
indoc = "2.0.5"

lib/src/install.rs

Lines changed: 72 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ use std::str::FromStr;
2020
use std::sync::Arc;
2121
use std::time::Duration;
2222

23-
use anyhow::Ok;
2423
use anyhow::{anyhow, Context, Result};
24+
use anyhow::{ensure, Ok};
2525
use bootc_utils::CommandRunExt;
2626
use camino::Utf8Path;
2727
use camino::Utf8PathBuf;
@@ -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,12 @@ 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+
ensure!(
1357+
crate::mount::is_same_as_host(Utf8Path::new("/dev"))?,
1358+
"Missing /dev mount to host /dev"
1359+
);
1360+
13391361
let bound_images = if state.config_opts.skip_bound_images {
13401362
Vec::new()
13411363
} else {
@@ -1356,8 +1378,16 @@ async fn install_to_filesystem_impl(state: &State, rootfs: &mut RootSetup) -> Re
13561378

13571379
// Initialize the ostree sysroot (repo, stateroot, etc.)
13581380
{
1359-
let sysroot = initialize_ostree_root(state, rootfs).await?;
1360-
install_with_sysroot(state, rootfs, &sysroot, &boot_uuid, &bound_images).await?;
1381+
let (sysroot, has_ostree) = initialize_ostree_root(state, rootfs).await?;
1382+
install_with_sysroot(
1383+
state,
1384+
rootfs,
1385+
&sysroot,
1386+
&boot_uuid,
1387+
&bound_images,
1388+
has_ostree,
1389+
)
1390+
.await?;
13611391
// We must drop the sysroot here in order to close any open file
13621392
// descriptors.
13631393
}
@@ -1498,7 +1528,8 @@ fn remove_all_in_dir_no_xdev(d: &Dir) -> Result<()> {
14981528

14991529
#[context("Removing boot directory content")]
15001530
fn clean_boot_directories(rootfs: &Dir) -> Result<()> {
1501-
let bootdir = rootfs.open_dir(BOOT).context("Opening /boot")?;
1531+
let bootdir =
1532+
crate::utils::open_dir_remount_rw(rootfs, BOOT.into()).context("Opening /boot")?;
15021533
// This should not remove /boot/efi note.
15031534
remove_all_in_dir_no_xdev(&bootdir)?;
15041535
if ARCH_USES_EFI {
@@ -1589,12 +1620,35 @@ pub(crate) async fn install_to_filesystem(
15891620
if !st.is_dir() {
15901621
anyhow::bail!("Not a directory: {root_path}");
15911622
}
1623+
1624+
let inspect = crate::mount::inspect_filesystem(&fsopts.root_path)?;
1625+
1626+
let alternative_root = fsopts.root_path.join("sysroot");
1627+
let root_path = match inspect.source.as_str() {
1628+
// Our target filesystem is an overlay, the true root is in `/sysroot`
1629+
"overlay" => {
1630+
tracing::debug!(
1631+
"Overlay filesystem detected, using {alternative_root} instead of {root_path} as target root"
1632+
);
1633+
&alternative_root
1634+
}
1635+
_ => root_path,
1636+
};
15921637
let rootfs_fd = Dir::open_ambient_dir(root_path, cap_std::ambient_authority())
15931638
.with_context(|| format!("Opening target root directory {root_path}"))?;
1639+
1640+
tracing::debug!("Root filesystem: {root_path}");
1641+
15941642
if let Some(false) = ostree_ext::mountutil::is_mountpoint(&rootfs_fd, ".")? {
15951643
anyhow::bail!("Not a mountpoint: {root_path}");
15961644
}
15971645

1646+
let fsopts = {
1647+
let mut fsopts = fsopts.clone();
1648+
fsopts.root_path = root_path.clone();
1649+
fsopts
1650+
};
1651+
15981652
// Gather global state, destructuring the provided options.
15991653
// IMPORTANT: We might re-execute the current process in this function (for SELinux among other things)
16001654
// IMPORTANT: and hence anything that is done before MUST BE IDEMPOTENT.

lib/src/lsm.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,11 @@ pub(crate) fn ensure_dir_labeled(
366366

367367
root.ensure_dir_with(local_destname, &DirBuilder::new())
368368
.with_context(|| format!("Opening {local_destname}"))?;
369+
// tracing::trace!("Rooted at {}", root.canonicalize()
370+
tracing::trace!(
371+
"Going to open {} for fchmod",
372+
local_destname.as_std_path().to_str().unwrap()
373+
);
369374
let dirfd = cap_std_ext::cap_primitives::fs::open(
370375
&root.as_filelike_view(),
371376
local_destname.as_std_path(),

lib/src/utils.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ 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::dirext::CapStdExtDirExt;
10+
use cap_std_ext::{cap_std::fs::Dir, prelude::CapStdExtCommandExt};
11+
use fn_error_context::context;
912
use ostree::glib;
1013
use ostree_ext::container::SignatureSource;
1114
use ostree_ext::ostree;
@@ -55,6 +58,22 @@ pub(crate) fn find_mount_option<'a>(
5558
.next()
5659
}
5760

61+
/// Given a target directory, if it's a read-only mount, then remount it writable
62+
#[context("Opening {target} with writable mount")]
63+
pub(crate) fn open_dir_remount_rw(root: &Dir, target: &Utf8Path) -> Result<Dir> {
64+
if matches!(root.is_mountpoint(target), Ok(Some(true))) {
65+
tracing::debug!("Target {target} is a mountpoint, remounting rw");
66+
let st = Command::new("mount")
67+
.args(["-o", "remount,rw", target.as_str()])
68+
.cwd_dir(root.try_clone()?)
69+
.status()?;
70+
if !st.success() {
71+
anyhow::bail!("Failed to remount: {st:?}");
72+
}
73+
}
74+
root.open_dir(target).map_err(anyhow::Error::new)
75+
}
76+
5877
pub(crate) fn spawn_editor(tmpf: &tempfile::NamedTempFile) -> Result<()> {
5978
let editor_variables = ["EDITOR"];
6079
// These roughly match https://github.com/systemd/systemd/blob/769ca9ab557b19ee9fb5c5106995506cace4c68f/src/shared/edit-util.c#L275

0 commit comments

Comments
 (0)