Skip to content

Commit e4f00af

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 7856ce0 commit e4f00af

File tree

2 files changed

+92
-15
lines changed

2 files changed

+92
-15
lines changed

lib/src/install.rs

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,7 @@ 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
@@ -578,11 +578,17 @@ async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result
578578

579579
let stateroot = state.stateroot();
580580

581-
Task::new_and_run(
582-
"Initializing ostree layout",
583-
"ostree",
584-
["admin", "init-fs", "--modern", rootfs.as_str()],
585-
)?;
581+
let has_ostree = rootfs_dir.try_exists("ostree/repo")?;
582+
if !has_ostree {
583+
Task::new_and_run(
584+
"Initializing ostree layout",
585+
"ostree",
586+
["admin", "init-fs", "--modern", rootfs.as_str()],
587+
)?;
588+
} else {
589+
println!("Reusing extant ostree layout");
590+
let _ = crate::utils::open_dir_remount_rw(rootfs_dir, "sysroot".into())?;
591+
}
586592

587593
// And also label /boot AKA xbootldr, if it exists
588594
let bootdir = rootfs.join("boot");
@@ -607,9 +613,14 @@ async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result
607613
let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(rootfs)));
608614
sysroot.load(cancellable)?;
609615

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

614625
let sysroot_dir = Dir::reopen_dir(&crate::utils::sysroot_fd(&sysroot))?;
615626

@@ -637,14 +648,15 @@ async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result
637648
let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(rootfs)));
638649
sysroot.load(cancellable)?;
639650
let sysroot = SysrootLock::new_from_sysroot(&sysroot).await?;
640-
Storage::new(sysroot, &temp_run)
651+
Ok((Storage::new(sysroot, &temp_run)?, has_ostree))
641652
}
642653

643654
#[context("Creating ostree deployment")]
644655
async fn install_container(
645656
state: &State,
646657
root_setup: &RootSetup,
647658
sysroot: &ostree::Sysroot,
659+
has_ostree: bool,
648660
) -> Result<(ostree::Deployment, InstallAleph)> {
649661
let sepolicy = state.load_policy()?;
650662
let sepolicy = sepolicy.as_ref();
@@ -730,6 +742,7 @@ async fn install_container(
730742
options.kargs = Some(kargs.as_slice());
731743
options.target_imgref = Some(&state.target_imgref);
732744
options.proxy_cfg = proxy_cfg;
745+
options.no_clean = has_ostree;
733746
let imgstate = crate::utils::async_task_with_spinner(
734747
"Deploying container image",
735748
ostree_container::deploy::deploy(&sysroot, stateroot, &src_imageref, Some(options)),
@@ -1275,10 +1288,11 @@ async fn install_with_sysroot(
12751288
sysroot: &Storage,
12761289
boot_uuid: &str,
12771290
bound_images: &[crate::boundimage::ResolvedBoundImage],
1291+
has_ostree: bool,
12781292
) -> Result<()> {
12791293
// And actually set up the container in that root, returning a deployment and
12801294
// the aleph state (see below).
1281-
let (_deployment, aleph) = install_container(state, rootfs, &sysroot).await?;
1295+
let (_deployment, aleph) = install_container(state, rootfs, &sysroot, has_ostree).await?;
12821296
// Write the aleph data that captures the system state at the time of provisioning for aid in future debugging.
12831297
rootfs
12841298
.rootfs_fd
@@ -1339,6 +1353,19 @@ async fn install_to_filesystem_impl(state: &State, rootfs: &mut RootSetup) -> Re
13391353
.ok_or_else(|| anyhow!("No uuid for boot/root"))?;
13401354
tracing::debug!("boot uuid={boot_uuid}");
13411355

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

13601387
// Initialize the ostree sysroot (repo, stateroot, etc.)
13611388
{
1362-
let sysroot = initialize_ostree_root(state, rootfs).await?;
1363-
install_with_sysroot(state, rootfs, &sysroot, &boot_uuid, &bound_images).await?;
1389+
let (sysroot, has_ostree) = initialize_ostree_root(state, rootfs).await?;
1390+
install_with_sysroot(
1391+
state,
1392+
rootfs,
1393+
&sysroot,
1394+
&boot_uuid,
1395+
&bound_images,
1396+
has_ostree,
1397+
)
1398+
.await?;
13641399
// We must drop the sysroot here in order to close any open file
13651400
// descriptors.
13661401
}
@@ -1501,7 +1536,8 @@ fn remove_all_in_dir_no_xdev(d: &Dir) -> Result<()> {
15011536

15021537
#[context("Removing boot directory content")]
15031538
fn clean_boot_directories(rootfs: &Dir) -> Result<()> {
1504-
let bootdir = rootfs.open_dir(BOOT).context("Opening /boot")?;
1539+
let bootdir =
1540+
crate::utils::open_dir_remount_rw(rootfs, BOOT.into()).context("Opening /boot")?;
15051541
// This should not remove /boot/efi note.
15061542
remove_all_in_dir_no_xdev(&bootdir)?;
15071543
if ARCH_USES_EFI {

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`.
@@ -54,6 +57,44 @@ pub(crate) fn find_mount_option<'a>(
5457
.next()
5558
}
5659

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

0 commit comments

Comments
 (0)