Skip to content

Commit 4a985e8

Browse files
composefs-backend: Introduce finalize-staged service
The service is intended to perform the following task at shutdown if a staged deployment is present - Perform three way /etc merge for the staged deployment - Un-stage boot entries depending upon bootloader and boot entry type Some assumptions made - ESP will always be present - We won't have two bootloaders at the same time Signed-off-by: Pragyan Poudyal <[email protected]>
1 parent 218ff2f commit 4a985e8

File tree

8 files changed

+259
-77
lines changed

8 files changed

+259
-77
lines changed

crates/initramfs/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,9 @@ pub fn mount_at_wrapper(
112112
.with_context(|| format!("Mounting at path {path:?}"))
113113
}
114114

115+
/// Wrapper around [`rustix::openat`]
115116
#[context("Opening dir {name:?}")]
116-
fn open_dir(dirfd: impl AsFd, name: impl AsRef<Path> + Debug) -> Result<OwnedFd> {
117+
pub fn open_dir(dirfd: impl AsFd, name: impl AsRef<Path> + Debug) -> Result<OwnedFd> {
117118
let res = openat(
118119
dirfd,
119120
name.as_ref(),

crates/lib/src/bootc_composefs/boot.rs

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,19 @@ pub fn get_esp_partition(device: &str) -> Result<(String, Option<String>)> {
125125
Ok((esp.node, esp.uuid))
126126
}
127127

128+
pub fn get_sysroot_parent_dev() -> Result<String> {
129+
let sysroot = Utf8PathBuf::from("/sysroot");
130+
131+
let fsinfo = inspect_filesystem(&sysroot)?;
132+
let parent_devices = find_parent_devices(&fsinfo.source)?;
133+
134+
let Some(parent) = parent_devices.into_iter().next() else {
135+
anyhow::bail!("Could not find parent device for mountpoint /sysroot");
136+
};
137+
138+
return Ok(parent);
139+
}
140+
128141
/// Compute SHA256Sum of VMlinuz + Initrd
129142
///
130143
/// # Arguments
@@ -310,20 +323,12 @@ pub(crate) fn setup_composefs_bls_boot(
310323
}
311324

312325
BootSetupType::Upgrade((fs, host)) => {
313-
let sysroot = Utf8PathBuf::from("/sysroot");
314-
315-
let fsinfo = inspect_filesystem(&sysroot)?;
316-
let parent_devices = find_parent_devices(&fsinfo.source)?;
317-
318-
let Some(parent) = parent_devices.into_iter().next() else {
319-
anyhow::bail!("Could not find parent device for mountpoint /sysroot");
320-
};
321-
326+
let sysroot_parent = get_sysroot_parent_dev()?;
322327
let bootloader = host.require_composefs_booted()?.bootloader.clone();
323328

324329
(
325330
Utf8PathBuf::from("/sysroot"),
326-
get_esp_partition(&parent)?.0,
331+
get_esp_partition(&sysroot_parent)?.0,
327332
[
328333
format!("root=UUID={DPS_UUID}"),
329334
RW_KARG.to_string(),
@@ -554,15 +559,9 @@ pub(crate) fn setup_composefs_uki_boot(
554559

555560
BootSetupType::Upgrade(..) => {
556561
let sysroot = Utf8PathBuf::from("/sysroot");
562+
let sysroot_parent = get_sysroot_parent_dev()?;
557563

558-
let fsinfo = inspect_filesystem(&sysroot)?;
559-
let parent_devices = find_parent_devices(&fsinfo.source)?;
560-
561-
let Some(parent) = parent_devices.into_iter().next() else {
562-
anyhow::bail!("Could not find parent device for mountpoint /sysroot");
563-
};
564-
565-
(sysroot, get_esp_partition(&parent)?.0, None)
564+
(sysroot, get_esp_partition(&sysroot_parent)?.0, None)
566565
}
567566
};
568567

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#![allow(dead_code)]
2+
3+
use std::path::Path;
4+
5+
use crate::bootc_composefs::boot::{get_esp_partition, get_sysroot_parent_dev, BootType};
6+
use crate::bootc_composefs::rollback::{rename_exchange_bls_entries, rename_exchange_user_cfg};
7+
use crate::spec::Bootloader;
8+
use crate::{
9+
bootc_composefs::status::composefs_deployment_status, composefs_consts::STATE_DIR_ABS,
10+
};
11+
use anyhow::{Context, Result};
12+
use bootc_initramfs_setup::{mount_at_wrapper, mount_composefs_image, open_dir};
13+
use bootc_mount::tempmount::TempMount;
14+
use cap_std_ext::cap_std::{ambient_authority, fs::Dir};
15+
use cap_std_ext::dirext::CapStdExtDirExt;
16+
use etc_merge::{compute_diff, merge, traverse_etc};
17+
use rustix::fs::{fsync, renameat, CWD};
18+
use rustix::mount::{unmount, UnmountFlags};
19+
use rustix::path::Arg;
20+
21+
use fn_error_context::context;
22+
23+
pub(crate) async fn composefs_native_finalize() -> Result<()> {
24+
let host = composefs_deployment_status().await?;
25+
26+
let booted_composefs = host.require_composefs_booted()?;
27+
28+
let Some(staged_depl) = host.status.staged.as_ref() else {
29+
tracing::debug!("No staged deployment found");
30+
return Ok(());
31+
};
32+
33+
let staged_composefs = staged_depl.composefs.as_ref().ok_or(anyhow::anyhow!(
34+
"Staged deployment is not a composefs deployment"
35+
))?;
36+
37+
// Mount the booted EROFS image to get pristine etc
38+
let sysroot = open_dir(CWD, "/sysroot")?;
39+
let composefs_fd = mount_composefs_image(&sysroot, &booted_composefs.verity, false)?;
40+
41+
let tempdir = tempfile::tempdir().context("Creating tempdir")?;
42+
mount_at_wrapper(composefs_fd, CWD, tempdir.path())?;
43+
44+
// Perform the /etc merge
45+
let pristine_etc = Dir::open_ambient_dir(tempdir.path(), ambient_authority())?;
46+
let current_etc = Dir::open_ambient_dir("/etc", ambient_authority())?;
47+
48+
let new_etc_path = Path::new(STATE_DIR_ABS)
49+
.join(&staged_composefs.verity)
50+
.join("etc");
51+
52+
let new_etc = Dir::open_ambient_dir(new_etc_path, ambient_authority())?;
53+
54+
let (pristine_files, current_files, new_files) =
55+
traverse_etc(&pristine_etc, &current_etc, &new_etc)?;
56+
57+
let diff = compute_diff(&pristine_files, &current_files)?;
58+
merge(&current_etc, &current_files, &new_etc, &new_files, diff)?;
59+
60+
unmount(tempdir.path(), UnmountFlags::DETACH).context("Unmounting tempdir")?;
61+
62+
let sysroot_parent = get_sysroot_parent_dev()?;
63+
// NOTE: Assumption here that ESP will always be present
64+
let (esp_part, ..) = get_esp_partition(&sysroot_parent)?;
65+
66+
let esp_mount = TempMount::mount_dev(&esp_part)?;
67+
let boot_dir = Dir::open_ambient_dir("/sysroot/boot", ambient_authority())
68+
.context("Opening sysroot/boot")?;
69+
70+
// NOTE: Assuming here we won't have two bootloaders at the same time
71+
match booted_composefs.bootloader {
72+
Bootloader::Grub => match staged_composefs.boot_type {
73+
BootType::Bls => {
74+
let entries_dir = boot_dir.open_dir("loader")?;
75+
rename_exchange_bls_entries(&entries_dir)?;
76+
}
77+
BootType::Uki => finalize_staged_grub_uki(&esp_mount.fd, &boot_dir)?,
78+
},
79+
80+
Bootloader::Systemd => match staged_composefs.boot_type {
81+
BootType::Bls => {
82+
let entries_dir = esp_mount.fd.open_dir("loader")?;
83+
rename_exchange_bls_entries(&entries_dir)?;
84+
}
85+
BootType::Uki => rename_staged_uki_entries(&esp_mount.fd)?,
86+
},
87+
};
88+
89+
Ok(())
90+
}
91+
92+
#[context("Grub: Finalizing staged UKI")]
93+
fn finalize_staged_grub_uki(esp_mount: &Dir, boot_fd: &Dir) -> Result<()> {
94+
rename_staged_uki_entries(esp_mount)?;
95+
96+
let entries_dir = boot_fd.open_dir("grub2")?;
97+
rename_exchange_user_cfg(&entries_dir)?;
98+
99+
let entries_dir = entries_dir.reopen_as_ownedfd()?;
100+
fsync(entries_dir).context("fsync")?;
101+
102+
Ok(())
103+
}
104+
105+
#[context("Renaming staged UKI entries")]
106+
fn rename_staged_uki_entries(esp_mount: &Dir) -> Result<()> {
107+
for entry in esp_mount.entries()? {
108+
let entry = entry?;
109+
110+
let filename = entry.file_name();
111+
let filename = filename.as_str()?;
112+
113+
if !filename.ends_with(".staged") {
114+
continue;
115+
}
116+
117+
renameat(
118+
&esp_mount,
119+
filename,
120+
&esp_mount,
121+
// SAFETY: We won't reach here if not for the above condition
122+
filename.strip_suffix(".staged").unwrap(),
123+
)
124+
.context("Renaming {filename}")?;
125+
}
126+
127+
let esp_mount = esp_mount.reopen_as_ownedfd()?;
128+
fsync(esp_mount).context("fsync")?;
129+
130+
Ok(())
131+
}

crates/lib/src/bootc_composefs/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ pub(crate) mod state;
55
pub(crate) mod status;
66
pub(crate) mod switch;
77
pub(crate) mod update;
8+
pub(crate) mod finalize;

crates/lib/src/bootc_composefs/rollback.rs

Lines changed: 55 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,60 @@ use crate::bootc_composefs::status::{composefs_deployment_status, get_sorted_bls
1212
use crate::{
1313
bootc_composefs::{boot::get_efi_uuid_source, status::get_sorted_uki_boot_entries},
1414
composefs_consts::{
15-
BOOT_LOADER_ENTRIES, ROLLBACK_BOOT_LOADER_ENTRIES, USER_CFG, USER_CFG_ROLLBACK,
15+
BOOT_LOADER_ENTRIES, STAGED_BOOT_LOADER_ENTRIES, USER_CFG, USER_CFG_STAGED,
1616
},
1717
spec::BootOrder,
1818
};
1919

20+
pub(crate) fn rename_exchange_user_cfg(entries_dir: &Dir) -> Result<()> {
21+
tracing::debug!("Atomically exchanging {USER_CFG_STAGED} and {USER_CFG}");
22+
renameat_with(
23+
&entries_dir,
24+
USER_CFG_STAGED,
25+
&entries_dir,
26+
USER_CFG,
27+
RenameFlags::EXCHANGE,
28+
)
29+
.context("renameat")?;
30+
31+
tracing::debug!("Removing {USER_CFG_STAGED}");
32+
rustix::fs::unlinkat(&entries_dir, USER_CFG_STAGED, AtFlags::empty()).context("unlinkat")?;
33+
34+
tracing::debug!("Syncing to disk");
35+
let entries_dir = entries_dir
36+
.reopen_as_ownedfd()
37+
.context(format!("Reopening entries dir as owned fd"))?;
38+
39+
fsync(entries_dir).context(format!("fsync entries dir"))?;
40+
41+
Ok(())
42+
}
43+
44+
pub(crate) fn rename_exchange_bls_entries(entries_dir: &Dir) -> Result<()> {
45+
tracing::debug!("Atomically exchanging {STAGED_BOOT_LOADER_ENTRIES} and {BOOT_LOADER_ENTRIES}");
46+
renameat_with(
47+
&entries_dir,
48+
STAGED_BOOT_LOADER_ENTRIES,
49+
&entries_dir,
50+
BOOT_LOADER_ENTRIES,
51+
RenameFlags::EXCHANGE,
52+
)
53+
.context("renameat")?;
54+
55+
tracing::debug!("Removing {STAGED_BOOT_LOADER_ENTRIES}");
56+
rustix::fs::unlinkat(&entries_dir, STAGED_BOOT_LOADER_ENTRIES, AtFlags::REMOVEDIR)
57+
.context("unlinkat")?;
58+
59+
tracing::debug!("Syncing to disk");
60+
let entries_dir = entries_dir
61+
.reopen_as_ownedfd()
62+
.with_context(|| format!("Reopening /sysroot/boot/loader as owned fd"))?;
63+
64+
fsync(entries_dir).context("fsync")?;
65+
66+
Ok(())
67+
}
68+
2069
#[context("Rolling back UKI")]
2170
pub(crate) fn rollback_composefs_uki() -> Result<()> {
2271
let user_cfg_path = PathBuf::from("/sysroot/boot/grub2");
@@ -45,31 +94,10 @@ pub(crate) fn rollback_composefs_uki() -> Result<()> {
4594
.with_context(|| format!("Opening {user_cfg_path:?}"))?;
4695

4796
entries_dir
48-
.atomic_write(USER_CFG_ROLLBACK, buffer)
49-
.with_context(|| format!("Writing to {USER_CFG_ROLLBACK}"))?;
50-
51-
tracing::debug!("Atomically exchanging for {USER_CFG_ROLLBACK} and {USER_CFG}");
52-
renameat_with(
53-
&entries_dir,
54-
USER_CFG_ROLLBACK,
55-
&entries_dir,
56-
USER_CFG,
57-
RenameFlags::EXCHANGE,
58-
)
59-
.context("renameat")?;
60-
61-
tracing::debug!("Removing {USER_CFG_ROLLBACK}");
62-
rustix::fs::unlinkat(&entries_dir, USER_CFG_ROLLBACK, AtFlags::empty()).context("unlinkat")?;
63-
64-
tracing::debug!("Syncing to disk");
65-
fsync(
66-
entries_dir
67-
.reopen_as_ownedfd()
68-
.with_context(|| format!("Reopening {user_cfg_path:?} as owned fd"))?,
69-
)
70-
.with_context(|| format!("fsync {user_cfg_path:?}"))?;
97+
.atomic_write(USER_CFG_STAGED, buffer)
98+
.with_context(|| format!("Writing to {USER_CFG_STAGED}"))?;
7199

72-
Ok(())
100+
rename_exchange_user_cfg(&entries_dir)
73101
}
74102

75103
#[context("Rolling back BLS")]
@@ -93,9 +121,7 @@ pub(crate) fn rollback_composefs_bls() -> Result<()> {
93121
assert!(all_configs.len() == 2);
94122

95123
// Write these
96-
let dir_path = PathBuf::from(format!(
97-
"/sysroot/boot/loader/{ROLLBACK_BOOT_LOADER_ENTRIES}",
98-
));
124+
let dir_path = PathBuf::from(format!("/sysroot/boot/loader/{STAGED_BOOT_LOADER_ENTRIES}",));
99125
create_dir_all(&dir_path).with_context(|| format!("Failed to create dir: {dir_path:?}"))?;
100126

101127
let rollback_entries_dir =
@@ -124,30 +150,7 @@ pub(crate) fn rollback_composefs_bls() -> Result<()> {
124150
let dir = Dir::open_ambient_dir("/sysroot/boot/loader", cap_std::ambient_authority())
125151
.context("Opening loader dir")?;
126152

127-
tracing::debug!(
128-
"Atomically exchanging for {ROLLBACK_BOOT_LOADER_ENTRIES} and {BOOT_LOADER_ENTRIES}"
129-
);
130-
renameat_with(
131-
&dir,
132-
ROLLBACK_BOOT_LOADER_ENTRIES,
133-
&dir,
134-
BOOT_LOADER_ENTRIES,
135-
RenameFlags::EXCHANGE,
136-
)
137-
.context("renameat")?;
138-
139-
tracing::debug!("Removing {ROLLBACK_BOOT_LOADER_ENTRIES}");
140-
rustix::fs::unlinkat(&dir, ROLLBACK_BOOT_LOADER_ENTRIES, AtFlags::empty())
141-
.context("unlinkat")?;
142-
143-
tracing::debug!("Syncing to disk");
144-
fsync(
145-
dir.reopen_as_ownedfd()
146-
.with_context(|| format!("Reopening /sysroot/boot/loader as owned fd"))?,
147-
)
148-
.context("fsync")?;
149-
150-
Ok(())
153+
rename_exchange_bls_entries(&dir)
151154
}
152155

153156
#[context("Rolling back composefs")]

crates/lib/src/cli.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ use serde::{Deserialize, Serialize};
3131

3232
#[cfg(feature = "composefs-backend")]
3333
use crate::bootc_composefs::{
34-
rollback::composefs_rollback, status::composefs_booted, switch::switch_composefs,
35-
update::upgrade_composefs,
34+
finalize::composefs_native_finalize, rollback::composefs_rollback, status::composefs_booted,
35+
switch::switch_composefs, update::upgrade_composefs,
3636
};
3737
use crate::deploy::RequiredHostSpec;
3838
use crate::lints;
@@ -712,6 +712,8 @@ pub(crate) enum Opt {
712712
#[clap(hide(true))]
713713
#[cfg(feature = "docgen")]
714714
Man(ManOpts),
715+
#[cfg(feature = "composefs-backend")]
716+
ComposefsFinalizeStaged,
715717
}
716718

717719
/// Ensure we've entered a mount namespace, so that we can remount
@@ -1575,6 +1577,9 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
15751577
Ok(())
15761578
}
15771579
},
1580+
1581+
#[cfg(feature = "composefs-backend")]
1582+
Opt::ComposefsFinalizeStaged => composefs_native_finalize().await,
15781583
}
15791584
}
15801585

crates/lib/src/composefs_consts.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,8 @@ pub(crate) const ORIGIN_KEY_BOOT_DIGEST: &str = "digest";
2626
pub(crate) const BOOT_LOADER_ENTRIES: &str = "entries";
2727
/// Filename for staged boot loader entries
2828
pub(crate) const STAGED_BOOT_LOADER_ENTRIES: &str = "entries.staged";
29-
/// Filename for rollback boot loader entries
30-
pub(crate) const ROLLBACK_BOOT_LOADER_ENTRIES: &str = STAGED_BOOT_LOADER_ENTRIES;
3129

3230
/// Filename for grub user config
3331
pub(crate) const USER_CFG: &str = "user.cfg";
3432
/// Filename for staged grub user config
3533
pub(crate) const USER_CFG_STAGED: &str = "user.cfg.staged";
36-
/// Filename for rollback grub user config
37-
pub(crate) const USER_CFG_ROLLBACK: &str = USER_CFG_STAGED;

0 commit comments

Comments
 (0)