Skip to content

Commit 86f6038

Browse files
committed
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: Colin Walters <walters@verbum.org>
1 parent bb163bc commit 86f6038

File tree

3 files changed

+81
-11
lines changed

3 files changed

+81
-11
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,6 @@ exclude-crate-paths = [ { name = "libz-sys", exclude = "src/zlib" },
3333
{ name = "k8s-openapi", exclude = "src/v1_25" },
3434
{ name = "k8s-openapi", exclude = "src/v1_27" },
3535
]
36+
37+
[patch.crates-io]
38+
ostree-ext = { path = "../../ostreedev/ostree-rs-ext/lib" }

lib/src/install.rs

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -459,11 +459,17 @@ async fn initialize_ostree_root_from_self(
459459

460460
// TODO: make configurable?
461461
let stateroot = STATEROOT_DEFAULT;
462-
Task::new_and_run(
463-
"Initializing ostree layout",
464-
"ostree",
465-
["admin", "init-fs", "--modern", rootfs.as_str()],
466-
)?;
462+
let has_ostree = rootfs_dir.try_exists("ostree/repo")?;
463+
if !has_ostree {
464+
Task::new_and_run(
465+
"Initializing ostree layout",
466+
"ostree",
467+
["admin", "init-fs", "--modern", rootfs.as_str()],
468+
)?;
469+
} else {
470+
println!("Reusing extant ostree layout");
471+
let _ = crate::utils::open_dir_remount_rw(rootfs_dir, "sysroot".into())?;
472+
}
467473

468474
// Default to avoiding grub2-mkconfig etc., but we need to use zipl on s390x.
469475
// TODO: Lower this logic into ostree proper.
@@ -482,10 +488,15 @@ async fn initialize_ostree_root_from_self(
482488
.quiet()
483489
.run()?;
484490
}
485-
Task::new("Initializing sysroot", "ostree")
486-
.args(["admin", "os-init", stateroot, "--sysroot", "."])
487-
.cwd(rootfs_dir)?
488-
.run()?;
491+
let stateroot_exists = rootfs_dir.try_exists(format!("ostree/deploy/{stateroot}"))?;
492+
if stateroot_exists {
493+
anyhow::bail!("Cannot redeploy over extant stateroot {stateroot}");
494+
} else {
495+
Task::new("Initializing sysroot", "ostree")
496+
.args(["admin", "os-init", stateroot, "--sysroot", "."])
497+
.cwd(rootfs_dir)?
498+
.run()?;
499+
}
489500

490501
// Ensure everything in the ostree repo is labeled
491502
state.lsm_label(&rootfs.join("ostree"), "/usr".into(), true)?;
@@ -532,6 +543,7 @@ async fn initialize_ostree_root_from_self(
532543
options.kargs = Some(kargs.as_slice());
533544
options.target_imgref = Some(&target_imgref);
534545
options.proxy_cfg = Some(proxy_cfg);
546+
options.no_clean = has_ostree;
535547
println!("Creating initial deployment");
536548
let state =
537549
ostree_container::deploy::deploy(&sysroot, stateroot, &src_imageref, Some(options)).await?;
@@ -845,14 +857,26 @@ async fn install_to_filesystem_impl(state: &State, rootfs: &mut RootSetup) -> Re
845857
}
846858

847859
let boot_uuid = rootfs.get_boot_uuid()?;
860+
// If we're doing an alongside install, then the /dev bootupd sees needs to be the host's.
861+
// What we probably really want to do here is tunnel in the host's /dev properly, but for now
862+
// just copy /dev/disk
863+
if rootfs.skip_finalize {
864+
if !Utf8Path::new("/dev/disk").try_exists()? {
865+
Task::new_and_run(
866+
"Copying host /dev/disk",
867+
"cp",
868+
["-a", "/proc/1/root/dev/disk", "/dev/disk"],
869+
)?;
870+
}
871+
}
848872
crate::bootloader::install_via_bootupd(&rootfs.device, &rootfs.rootfs, boot_uuid)?;
849873
tracing::debug!("Installed bootloader");
850874

851875
// If Ignition is specified, enable it
852876
if let Some(ignition_file) = state.config_opts.ignition_file.as_deref() {
853877
let src = std::fs::File::open(ignition_file)
854878
.with_context(|| format!("Opening {ignition_file}"))?;
855-
let bootfs = rootfs.rootfs.join("boot");
879+
let bootfs = rootfs.rootfs.join(gi"boot");
856880
crate::ignition::write_ignition(&bootfs, &state.config_opts.ignition_hash, &src)?;
857881
crate::ignition::enable_firstboot(&bootfs)?;
858882
println!("Installed Ignition config from {ignition_file}");
@@ -958,7 +982,8 @@ fn remove_all_in_dir_no_xdev(d: &Dir) -> Result<()> {
958982

959983
#[context("Removing boot directory content")]
960984
fn clean_boot_directories(rootfs: &Dir) -> Result<()> {
961-
let bootdir = rootfs.open_dir(BOOT).context("Opening /boot")?;
985+
let bootdir =
986+
crate::utils::open_dir_remount_rw(rootfs, BOOT.into()).context("Opening /boot")?;
962987
// This should not remove /boot/efi note.
963988
remove_all_in_dir_no_xdev(&bootdir)?;
964989
if ARCH_USES_EFI {

lib/src/utils.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ use std::os::unix::prelude::OsStringExt;
22
use std::process::Command;
33

44
use anyhow::{Context, Result};
5+
use camino::Utf8Path;
6+
use cap_std_ext::{cap_std::fs::Dir, prelude::CapStdExtCommandExt};
7+
use fn_error_context::context;
58
use ostree::glib;
69
use ostree_ext::ostree;
10+
use std::os::fd::AsFd;
711

812
/// Try to look for keys injected by e.g. rpm-ostree requesting machine-local
913
/// changes; if any are present, return `true`.
@@ -32,6 +36,44 @@ pub(crate) fn find_mount_option<'a>(
3236
.next()
3337
}
3438

39+
/// Try to (heuristically) determine if the provided path is a mount root.
40+
pub(crate) fn is_mountpoint(root: &Dir, path: &Utf8Path) -> Result<Option<bool>> {
41+
// https://github.com/systemd/systemd/blob/8fbf0a214e2fe474655b17a4b663122943b55db0/src/basic/mountpoint-util.c#L176
42+
use rustix::fs::{AtFlags, StatxFlags};
43+
44+
// SAFETY(unwrap): We can infallibly convert an i32 into a u64.
45+
let mountroot_flag: u64 = libc::STATX_ATTR_MOUNT_ROOT.try_into().unwrap();
46+
match rustix::fs::statx(
47+
root.as_fd(),
48+
path.as_std_path(),
49+
AtFlags::NO_AUTOMOUNT | AtFlags::SYMLINK_NOFOLLOW,
50+
StatxFlags::empty(),
51+
) {
52+
Ok(r) => {
53+
let present = (r.stx_attributes_mask & mountroot_flag) > 0;
54+
Ok(present.then(|| r.stx_attributes & mountroot_flag > 0))
55+
}
56+
Err(e) if e == rustix::io::Errno::NOSYS => Ok(None),
57+
Err(e) => Err(e.into()),
58+
}
59+
}
60+
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 is_mountpoint(root, target)?.unwrap_or_default() {
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+
3577
/// Run a command in the host mount namespace
3678
#[allow(dead_code)]
3779
pub(crate) fn run_in_host_mountns(cmd: &str) -> Command {

0 commit comments

Comments
 (0)