Skip to content

Commit dd7b49f

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 093ddc6 commit dd7b49f

File tree

4 files changed

+108
-16
lines changed

4 files changed

+108
-16
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
indoc = "2.0.5"

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;
@@ -574,26 +574,34 @@ pub(crate) fn print_configuration() -> Result<()> {
574574
}
575575

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

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

589-
let stateroot = state.stateroot();
590-
591-
Task::new_and_run(
592-
"Initializing ostree layout",
593-
"ostree",
594-
["admin", "init-fs", "--modern", rootfs.as_str()],
595-
)?;
596-
597605
// And also label /boot AKA xbootldr, if it exists
598606
let bootdir = rootfs.join("boot");
599607
if bootdir.try_exists()? {
@@ -617,6 +625,11 @@ async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result
617625
let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(rootfs)));
618626
sysroot.load(cancellable)?;
619627

628+
let stateroot_exists = rootfs_dir.try_exists(format!("ostree/deploy/{stateroot}"))?;
629+
ensure!(
630+
!stateroot_exists,
631+
"Cannot redeploy over extant stateroot {stateroot}"
632+
);
620633
sysroot
621634
.init_osname(stateroot, cancellable)
622635
.context("initializing stateroot")?;
@@ -647,14 +660,15 @@ async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result
647660
let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(rootfs)));
648661
sysroot.load(cancellable)?;
649662
let sysroot = SysrootLock::new_from_sysroot(&sysroot).await?;
650-
Storage::new(sysroot, &temp_run)
663+
Ok((Storage::new(sysroot, &temp_run)?, has_ostree))
651664
}
652665

653666
#[context("Creating ostree deployment")]
654667
async fn install_container(
655668
state: &State,
656669
root_setup: &RootSetup,
657670
sysroot: &ostree::Sysroot,
671+
has_ostree: bool,
658672
) -> Result<(ostree::Deployment, InstallAleph)> {
659673
let sepolicy = state.load_policy()?;
660674
let sepolicy = sepolicy.as_ref();
@@ -740,6 +754,7 @@ async fn install_container(
740754
options.kargs = Some(kargs.as_slice());
741755
options.target_imgref = Some(&state.target_imgref);
742756
options.proxy_cfg = proxy_cfg;
757+
options.no_clean = has_ostree;
743758
let imgstate = crate::utils::async_task_with_spinner(
744759
"Deploying container image",
745760
ostree_container::deploy::deploy(&sysroot, stateroot, &src_imageref, Some(options)),
@@ -1282,10 +1297,11 @@ async fn install_with_sysroot(
12821297
sysroot: &Storage,
12831298
boot_uuid: &str,
12841299
bound_images: &[crate::boundimage::ResolvedBoundImage],
1300+
has_ostree: bool,
12851301
) -> Result<()> {
12861302
// And actually set up the container in that root, returning a deployment and
12871303
// the aleph state (see below).
1288-
let (_deployment, aleph) = install_container(state, rootfs, &sysroot).await?;
1304+
let (_deployment, aleph) = install_container(state, rootfs, &sysroot, has_ostree).await?;
12891305
// Write the aleph data that captures the system state at the time of provisioning for aid in future debugging.
12901306
rootfs
12911307
.rootfs_fd
@@ -1346,6 +1362,12 @@ async fn install_to_filesystem_impl(state: &State, rootfs: &mut RootSetup) -> Re
13461362
.ok_or_else(|| anyhow!("No uuid for boot/root"))?;
13471363
tracing::debug!("boot uuid={boot_uuid}");
13481364

1365+
// If we're doing an alongside install, then the /dev bootupd sees needs to be the host's.
1366+
ensure!(
1367+
crate::mount::is_same_as_host(Utf8Path::new("/dev"))?,
1368+
"Missing /dev mount to host /dev"
1369+
);
1370+
13491371
let bound_images = if state.config_opts.skip_bound_images {
13501372
Vec::new()
13511373
} else {
@@ -1366,8 +1388,16 @@ async fn install_to_filesystem_impl(state: &State, rootfs: &mut RootSetup) -> Re
13661388

13671389
// Initialize the ostree sysroot (repo, stateroot, etc.)
13681390
{
1369-
let sysroot = initialize_ostree_root(state, rootfs).await?;
1370-
install_with_sysroot(state, rootfs, &sysroot, &boot_uuid, &bound_images).await?;
1391+
let (sysroot, has_ostree) = initialize_ostree_root(state, rootfs).await?;
1392+
install_with_sysroot(
1393+
state,
1394+
rootfs,
1395+
&sysroot,
1396+
&boot_uuid,
1397+
&bound_images,
1398+
has_ostree,
1399+
)
1400+
.await?;
13711401
// We must drop the sysroot here in order to close any open file
13721402
// descriptors.
13731403
}
@@ -1508,7 +1538,8 @@ fn remove_all_in_dir_no_xdev(d: &Dir) -> Result<()> {
15081538

15091539
#[context("Removing boot directory content")]
15101540
fn clean_boot_directories(rootfs: &Dir) -> Result<()> {
1511-
let bootdir = rootfs.open_dir(BOOT).context("Opening /boot")?;
1541+
let bootdir =
1542+
crate::utils::open_dir_remount_rw(rootfs, BOOT.into()).context("Opening /boot")?;
15121543
// This should not remove /boot/efi note.
15131544
remove_all_in_dir_no_xdev(&bootdir)?;
15141545
if ARCH_USES_EFI {
@@ -1599,12 +1630,35 @@ pub(crate) async fn install_to_filesystem(
15991630
if !st.is_dir() {
16001631
anyhow::bail!("Not a directory: {root_path}");
16011632
}
1633+
1634+
let inspect = crate::mount::inspect_filesystem(&fsopts.root_path)?;
1635+
1636+
let alternative_root = fsopts.root_path.join("sysroot");
1637+
let root_path = match inspect.fstype.as_str() {
1638+
// Our target filesystem is an overlay, the true root is in `/sysroot`
1639+
"overlay" => {
1640+
tracing::debug!(
1641+
"Overlay filesystem detected, using {alternative_root} instead of {root_path} as target root"
1642+
);
1643+
&alternative_root
1644+
}
1645+
_ => root_path,
1646+
};
16021647
let rootfs_fd = Dir::open_ambient_dir(root_path, cap_std::ambient_authority())
16031648
.with_context(|| format!("Opening target root directory {root_path}"))?;
1649+
1650+
tracing::debug!("Root filesystem: {root_path}");
1651+
16041652
if let Some(false) = ostree_ext::mountutil::is_mountpoint(&rootfs_fd, ".")? {
16051653
anyhow::bail!("Not a mountpoint: {root_path}");
16061654
}
16071655

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

lib/src/utils.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ use std::process::Command;
55
use std::time::Duration;
66

77
use anyhow::{Context, Result};
8+
use camino::Utf8Path;
89
use cap_std_ext::cap_std::fs::Dir;
10+
use cap_std_ext::dirext::CapStdExtDirExt;
11+
use cap_std_ext::{cap_std::fs::Dir, prelude::CapStdExtCommandExt};
12+
use fn_error_context::context;
913
use indicatif::HumanDuration;
1014
use libsystemd::logging::journal_print;
1115
use ostree::glib;
@@ -57,6 +61,22 @@ pub(crate) fn find_mount_option<'a>(
5761
.next()
5862
}
5963

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

tests-integration/src/install.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pub(crate) const BASE_ARGS: &[&str] = &[
2727

2828
// Arbitrary
2929
const NON_DEFAULT_STATEROOT: &str = "foo";
30+
const SOME_OTHER_STATEROOT: &str = "bar";
3031

3132
/// Clear out and delete any ostree roots, leverage bootc hidden wipe-ostree command to get rid of
3233
/// otherwise hard to delete deployment files
@@ -157,6 +158,23 @@ pub(crate) fn run_alongside(image: &str, mut testargs: libtest_mimic::Arguments)
157158
);
158159
Ok(())
159160
}),
161+
Trial::test("Install on already ostree target", move || {
162+
// Do an initial install just to get ostree on our system
163+
let sh = &xshell::Shell::new()?;
164+
reset_root(sh, image)?;
165+
cmd!(sh, "sudo {BASE_ARGS...} {target_args...} {image} bootc install to-existing-root --stateroot {NON_DEFAULT_STATEROOT} --acknowledge-destructive {generic_inst_args...}").run()?;
166+
generic_post_install_verification()?;
167+
assert!(
168+
Utf8Path::new(&format!("/ostree/deploy/{NON_DEFAULT_STATEROOT}")).try_exists()?
169+
);
170+
171+
// Now try again to a different stateroot
172+
let sh = &xshell::Shell::new()?;
173+
cmd!(sh, "sudo {BASE_ARGS...} {target_args...} {image} bootc install to-existing-root --replace alongside --stateroot {SOME_OTHER_STATEROOT} --acknowledge-destructive {generic_inst_args...}").run()?;
174+
generic_post_install_verification()?;
175+
assert!(Utf8Path::new(&format!("/ostree/deploy/{SOME_OTHER_STATEROOT}")).try_exists()?);
176+
Ok(())
177+
}),
160178
Trial::test("without an install config", move || {
161179
let sh = &xshell::Shell::new()?;
162180
reset_root(sh, image)?;

0 commit comments

Comments
 (0)