Skip to content

Commit b32fdf5

Browse files
cgwaltersomertuc
authored andcommitted
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 c1ac763 commit b32fdf5

File tree

4 files changed

+121
-22
lines changed

4 files changed

+121
-22
lines changed

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
clap_mangen = { version = "0.2.20" }

lib/src/install.rs

Lines changed: 69 additions & 15 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;
@@ -576,26 +576,36 @@ pub(crate) fn print_configuration() -> Result<()> {
576576
}
577577

578578
#[context("Creating ostree deployment")]
579-
async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result<Storage> {
579+
async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result<(Storage, bool)> {
580580
let sepolicy = state.load_policy()?;
581581
let sepolicy = sepolicy.as_ref();
582582
// Load a fd for the mounted target physical root
583583
let rootfs_dir = &root_setup.rootfs_fd;
584584
let rootfs = root_setup.rootfs.as_path();
585585
let cancellable = gio::Cancellable::NONE;
586586

587+
let stateroot = state.stateroot();
588+
589+
let has_ostree = rootfs_dir.try_exists("ostree/repo")?;
590+
if !has_ostree {
591+
Task::new_and_run(
592+
"Initializing ostree layout",
593+
"ostree",
594+
["admin", "init-fs", "--modern", rootfs.as_str()],
595+
)?;
596+
} else {
597+
println!("Reusing extant ostree layout");
598+
599+
let path = ".".into();
600+
let _ = crate::utils::open_dir_remount_rw(rootfs_dir, path)
601+
.context("remounting target as read-write")?;
602+
crate::utils::remove_immutability(rootfs_dir, path)?;
603+
}
604+
587605
// Ensure that the physical root is labeled.
588606
// Another implementation: https://github.com/coreos/coreos-assembler/blob/3cd3307904593b3a131b81567b13a4d0b6fe7c90/src/create_disk.sh#L295
589607
crate::lsm::ensure_dir_labeled(rootfs_dir, "", Some("/".into()), 0o755.into(), sepolicy)?;
590608

591-
let stateroot = state.stateroot();
592-
593-
Task::new_and_run(
594-
"Initializing ostree layout",
595-
"ostree",
596-
["admin", "init-fs", "--modern", rootfs.as_str()],
597-
)?;
598-
599609
// And also label /boot AKA xbootldr, if it exists
600610
let bootdir = rootfs.join("boot");
601611
if bootdir.try_exists()? {
@@ -619,6 +629,11 @@ async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result
619629
let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(rootfs)));
620630
sysroot.load(cancellable)?;
621631

632+
let stateroot_exists = rootfs_dir.try_exists(format!("ostree/deploy/{stateroot}"))?;
633+
ensure!(
634+
!stateroot_exists,
635+
"Cannot redeploy over extant stateroot {stateroot}"
636+
);
622637
sysroot
623638
.init_osname(stateroot, cancellable)
624639
.context("initializing stateroot")?;
@@ -649,14 +664,15 @@ async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result
649664
let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(rootfs)));
650665
sysroot.load(cancellable)?;
651666
let sysroot = SysrootLock::new_from_sysroot(&sysroot).await?;
652-
Storage::new(sysroot, &temp_run)
667+
Ok((Storage::new(sysroot, &temp_run)?, has_ostree))
653668
}
654669

655670
#[context("Creating ostree deployment")]
656671
async fn install_container(
657672
state: &State,
658673
root_setup: &RootSetup,
659674
sysroot: &ostree::Sysroot,
675+
has_ostree: bool,
660676
) -> Result<(ostree::Deployment, InstallAleph)> {
661677
let sepolicy = state.load_policy()?;
662678
let sepolicy = sepolicy.as_ref();
@@ -749,6 +765,7 @@ async fn install_container(
749765
options.kargs = Some(kargs.as_slice());
750766
options.target_imgref = Some(&state.target_imgref);
751767
options.proxy_cfg = proxy_cfg;
768+
options.no_clean = has_ostree;
752769
let imgstate = crate::utils::async_task_with_spinner(
753770
"Deploying container image",
754771
ostree_container::deploy::deploy(&sysroot, stateroot, &src_imageref, Some(options)),
@@ -1295,10 +1312,11 @@ async fn install_with_sysroot(
12951312
sysroot: &Storage,
12961313
boot_uuid: &str,
12971314
bound_images: &[crate::boundimage::ResolvedBoundImage],
1315+
has_ostree: bool,
12981316
) -> Result<()> {
12991317
// And actually set up the container in that root, returning a deployment and
13001318
// the aleph state (see below).
1301-
let (_deployment, aleph) = install_container(state, rootfs, &sysroot).await?;
1319+
let (_deployment, aleph) = install_container(state, rootfs, &sysroot, has_ostree).await?;
13021320
// Write the aleph data that captures the system state at the time of provisioning for aid in future debugging.
13031321
rootfs
13041322
.rootfs_fd
@@ -1359,6 +1377,12 @@ async fn install_to_filesystem_impl(state: &State, rootfs: &mut RootSetup) -> Re
13591377
.ok_or_else(|| anyhow!("No uuid for boot/root"))?;
13601378
tracing::debug!("boot uuid={boot_uuid}");
13611379

1380+
// If we're doing an alongside install, then the /dev bootupd sees needs to be the host's.
1381+
ensure!(
1382+
crate::mount::is_same_as_host(Utf8Path::new("/dev"))?,
1383+
"Missing /dev mount to host /dev"
1384+
);
1385+
13621386
let bound_images = if state.config_opts.skip_bound_images {
13631387
Vec::new()
13641388
} else {
@@ -1379,8 +1403,16 @@ async fn install_to_filesystem_impl(state: &State, rootfs: &mut RootSetup) -> Re
13791403

13801404
// Initialize the ostree sysroot (repo, stateroot, etc.)
13811405
{
1382-
let sysroot = initialize_ostree_root(state, rootfs).await?;
1383-
install_with_sysroot(state, rootfs, &sysroot, &boot_uuid, &bound_images).await?;
1406+
let (sysroot, has_ostree) = initialize_ostree_root(state, rootfs).await?;
1407+
install_with_sysroot(
1408+
state,
1409+
rootfs,
1410+
&sysroot,
1411+
&boot_uuid,
1412+
&bound_images,
1413+
has_ostree,
1414+
)
1415+
.await?;
13841416
// We must drop the sysroot here in order to close any open file
13851417
// descriptors.
13861418
}
@@ -1521,7 +1553,8 @@ fn remove_all_in_dir_no_xdev(d: &Dir) -> Result<()> {
15211553

15221554
#[context("Removing boot directory content")]
15231555
fn clean_boot_directories(rootfs: &Dir) -> Result<()> {
1524-
let bootdir = rootfs.open_dir(BOOT).context("Opening /boot")?;
1556+
let bootdir =
1557+
crate::utils::open_dir_remount_rw(rootfs, BOOT.into()).context("Opening /boot")?;
15251558
// This should not remove /boot/efi note.
15261559
remove_all_in_dir_no_xdev(&bootdir)?;
15271560
if ARCH_USES_EFI {
@@ -1612,12 +1645,33 @@ pub(crate) async fn install_to_filesystem(
16121645
if !st.is_dir() {
16131646
anyhow::bail!("Not a directory: {root_path}");
16141647
}
1648+
1649+
let possible_physical_root = fsopts.root_path.join("sysroot");
1650+
let possible_ostree_dir = possible_physical_root.join("ostree");
1651+
let root_path = if possible_ostree_dir.exists() {
1652+
tracing::debug!(
1653+
"ostree detected in {possible_ostree_dir}, assuming / is a deployment root and using {possible_physical_root} instead of {root_path} as target root"
1654+
);
1655+
&possible_physical_root
1656+
} else {
1657+
root_path
1658+
};
1659+
16151660
let rootfs_fd = Dir::open_ambient_dir(root_path, cap_std::ambient_authority())
16161661
.with_context(|| format!("Opening target root directory {root_path}"))?;
1662+
1663+
tracing::debug!("Root filesystem: {root_path}");
1664+
16171665
if let Some(false) = ostree_ext::mountutil::is_mountpoint(&rootfs_fd, ".")? {
16181666
anyhow::bail!("Not a mountpoint: {root_path}");
16191667
}
16201668

1669+
let fsopts = {
1670+
let mut fsopts = fsopts.clone();
1671+
fsopts.root_path = root_path.clone();
1672+
fsopts
1673+
};
1674+
16211675
// Gather global state, destructuring the provided options.
16221676
// IMPORTANT: We might re-execute the current process in this function (for SELinux among other things)
16231677
// IMPORTANT: and hence anything that is done before MUST BE IDEMPOTENT.

lib/src/utils.rs

Lines changed: 37 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 indicatif::HumanDuration;
1013
use libsystemd::logging::journal_print;
1114
use ostree::glib;
@@ -57,6 +60,39 @@ pub(crate) fn find_mount_option<'a>(
5760
.next()
5861
}
5962

63+
/// Given a target directory, if it's a read-only mount, then remount it writable
64+
#[context("Opening {target} with writable mount")]
65+
#[cfg(feature = "install")]
66+
pub(crate) fn open_dir_remount_rw(root: &Dir, target: &Utf8Path) -> Result<Dir> {
67+
if matches!(root.is_mountpoint(target), Ok(Some(true))) {
68+
tracing::debug!("Target {target} is a mountpoint, remounting rw");
69+
let st = Command::new("mount")
70+
.args(["-o", "remount,rw", target.as_str()])
71+
.cwd_dir(root.try_clone()?)
72+
.status()?;
73+
74+
anyhow::ensure!(st.success(), "Failed to remount: {st:?}");
75+
}
76+
root.open_dir(target).map_err(anyhow::Error::new)
77+
}
78+
79+
/// Given a target path, remove its immutability if present
80+
#[context("Removing immutable flag from {target}")]
81+
#[cfg(feature = "install")]
82+
pub(crate) fn remove_immutability(root: &Dir, target: &Utf8Path) -> Result<()> {
83+
use anyhow::ensure;
84+
85+
tracing::debug!("Target {target} is a mountpoint, remounting rw");
86+
let st = Command::new("chattr")
87+
.args(["-i", target.as_str()])
88+
.cwd_dir(root.try_clone()?)
89+
.status()?;
90+
91+
ensure!(st.success(), "Failed to remove immutability: {st:?}");
92+
93+
Ok(())
94+
}
95+
6096
pub(crate) fn spawn_editor(tmpf: &tempfile::NamedTempFile) -> Result<()> {
6197
let editor_variables = ["EDITOR"];
6298
// These roughly match https://github.com/systemd/systemd/blob/769ca9ab557b19ee9fb5c5106995506cace4c68f/src/shared/edit-util.c#L275

tests-integration/src/install.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,29 @@ const NON_DEFAULT_STATEROOT: &str = "foo";
3131
/// Clear out and delete any ostree roots, leverage bootc hidden wipe-ostree command to get rid of
3232
/// otherwise hard to delete deployment files
3333
fn reset_root(sh: &Shell, image: &str) -> Result<()> {
34-
if !Path::new("/ostree/deploy/").exists() {
34+
delete_ostree_deployments(sh, image)?;
35+
delete_ostree(sh)?;
36+
Ok(())
37+
}
38+
39+
fn delete_ostree(sh: &Shell) -> Result<(), anyhow::Error> {
40+
if !Path::new("/ostree/").exists() {
3541
return Ok(());
3642
}
43+
cmd!(sh, "sudo /bin/sh -c 'rm -rf /ostree/'").run()?;
44+
Ok(())
45+
}
3746

38-
// Without /boot ostree will not delete anything
47+
fn delete_ostree_deployments(sh: &Shell, image: &str) -> Result<(), anyhow::Error> {
48+
if !Path::new("/ostree/deploy/").exists() {
49+
return Ok(());
50+
}
3951
let mounts = &["-v", "/ostree:/ostree", "-v", "/boot:/boot"];
40-
4152
cmd!(
4253
sh,
4354
"sudo {BASE_ARGS...} {mounts...} {image} bootc state wipe-ostree"
4455
)
4556
.run()?;
46-
47-
// Now that the hard to delete files are gone, we can just rm -rf the rest
4857
cmd!(sh, "sudo /bin/sh -c 'rm -rf /ostree/deploy/*'").run()?;
4958
Ok(())
5059
}

0 commit comments

Comments
 (0)