Skip to content

Commit bda61be

Browse files
authored
Merge pull request #1599 from Johan-Liebert1/composefs-native-refactor
Composefs native refactor
2 parents ab40a99 + adb73a9 commit bda61be

File tree

18 files changed

+2036
-1828
lines changed

18 files changed

+2036
-1828
lines changed

crates/lib/src/bootc_composefs/boot.rs

Lines changed: 797 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1+
pub(crate) mod boot;
2+
pub(crate) mod repo;
3+
pub(crate) mod rollback;
14
pub(crate) mod state;
5+
pub(crate) mod status;
6+
pub(crate) mod switch;
7+
pub(crate) mod update;
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
use fn_error_context::context;
2+
use std::sync::Arc;
3+
4+
use anyhow::{Context, Result};
5+
6+
use ostree_ext::composefs::{
7+
fsverity::{FsVerityHashValue, Sha256HashValue},
8+
repository::Repository as ComposefsRepository,
9+
tree::FileSystem,
10+
util::Sha256Digest,
11+
};
12+
use ostree_ext::composefs_boot::{bootloader::BootEntry as ComposefsBootEntry, BootOps};
13+
use ostree_ext::composefs_oci::{
14+
image::create_filesystem as create_composefs_filesystem, pull as composefs_oci_pull,
15+
};
16+
17+
use ostree_ext::container::ImageReference as OstreeExtImgRef;
18+
19+
use cap_std_ext::cap_std::{ambient_authority, fs::Dir};
20+
21+
use crate::install::{RootSetup, State};
22+
23+
pub(crate) fn open_composefs_repo(
24+
rootfs_dir: &Dir,
25+
) -> Result<ComposefsRepository<Sha256HashValue>> {
26+
ComposefsRepository::open_path(rootfs_dir, "composefs")
27+
.context("Failed to open composefs repository")
28+
}
29+
30+
pub(crate) async fn initialize_composefs_repository(
31+
state: &State,
32+
root_setup: &RootSetup,
33+
) -> Result<(Sha256Digest, impl FsVerityHashValue)> {
34+
let rootfs_dir = &root_setup.physical_root;
35+
36+
rootfs_dir
37+
.create_dir_all("composefs")
38+
.context("Creating dir composefs")?;
39+
40+
let repo = open_composefs_repo(rootfs_dir)?;
41+
42+
let OstreeExtImgRef {
43+
name: image_name,
44+
transport,
45+
} = &state.source.imageref;
46+
47+
// transport's display is already of type "<transport_type>:"
48+
composefs_oci_pull(
49+
&Arc::new(repo),
50+
&format!("{transport}{image_name}"),
51+
None,
52+
None,
53+
)
54+
.await
55+
}
56+
57+
/// Pulls the `image` from `transport` into a composefs repository at /sysroot
58+
/// Checks for boot entries in the image and returns them
59+
#[context("Pulling composefs repository")]
60+
pub(crate) async fn pull_composefs_repo(
61+
transport: &String,
62+
image: &String,
63+
) -> Result<(
64+
ComposefsRepository<Sha256HashValue>,
65+
Vec<ComposefsBootEntry<Sha256HashValue>>,
66+
Sha256HashValue,
67+
FileSystem<Sha256HashValue>,
68+
)> {
69+
let rootfs_dir = Dir::open_ambient_dir("/sysroot", ambient_authority())?;
70+
71+
let repo = open_composefs_repo(&rootfs_dir).context("Opening compoesfs repo")?;
72+
73+
let (id, verity) =
74+
composefs_oci_pull(&Arc::new(repo), &format!("{transport}:{image}"), None, None)
75+
.await
76+
.context("Pulling composefs repo")?;
77+
78+
tracing::info!("id: {}, verity: {}", hex::encode(id), verity.to_hex());
79+
80+
let repo = open_composefs_repo(&rootfs_dir)?;
81+
let mut fs = create_composefs_filesystem(&repo, &hex::encode(id), None)
82+
.context("Failed to create composefs filesystem")?;
83+
84+
let entries = fs.transform_for_boot(&repo)?;
85+
let id = fs.commit_image(&repo, None)?;
86+
87+
Ok((repo, entries, id, fs))
88+
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
use std::path::PathBuf;
2+
use std::{fmt::Write, fs::create_dir_all};
3+
4+
use anyhow::{anyhow, Context, Result};
5+
use cap_std_ext::cap_std::fs::Dir;
6+
use cap_std_ext::{cap_std, dirext::CapStdExtDirExt};
7+
use fn_error_context::context;
8+
use rustix::fs::{fsync, renameat_with, AtFlags, RenameFlags};
9+
10+
use crate::bootc_composefs::boot::BootType;
11+
use crate::bootc_composefs::status::{composefs_deployment_status, get_sorted_bls_boot_entries};
12+
use crate::{
13+
bootc_composefs::{boot::get_efi_uuid_source, status::get_sorted_uki_boot_entries},
14+
composefs_consts::{
15+
BOOT_LOADER_ENTRIES, ROLLBACK_BOOT_LOADER_ENTRIES, USER_CFG, USER_CFG_ROLLBACK,
16+
},
17+
spec::BootOrder,
18+
};
19+
20+
#[context("Rolling back UKI")]
21+
pub(crate) fn rollback_composefs_uki() -> Result<()> {
22+
let user_cfg_path = PathBuf::from("/sysroot/boot/grub2");
23+
24+
let mut str = String::new();
25+
let boot_dir =
26+
cap_std::fs::Dir::open_ambient_dir("/sysroot/boot", cap_std::ambient_authority())
27+
.context("Opening boot dir")?;
28+
let mut menuentries =
29+
get_sorted_uki_boot_entries(&boot_dir, &mut str).context("Getting UKI boot entries")?;
30+
31+
// TODO(Johan-Liebert): Currently assuming there are only two deployments
32+
assert!(menuentries.len() == 2);
33+
34+
let (first, second) = menuentries.split_at_mut(1);
35+
std::mem::swap(&mut first[0], &mut second[0]);
36+
37+
let mut buffer = get_efi_uuid_source();
38+
39+
for entry in menuentries {
40+
write!(buffer, "{entry}")?;
41+
}
42+
43+
let entries_dir =
44+
cap_std::fs::Dir::open_ambient_dir(&user_cfg_path, cap_std::ambient_authority())
45+
.with_context(|| format!("Opening {user_cfg_path:?}"))?;
46+
47+
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:?}"))?;
71+
72+
Ok(())
73+
}
74+
75+
#[context("Rolling back BLS")]
76+
pub(crate) fn rollback_composefs_bls() -> Result<()> {
77+
let boot_dir =
78+
cap_std::fs::Dir::open_ambient_dir("/sysroot/boot", cap_std::ambient_authority())
79+
.context("Opening boot dir")?;
80+
81+
// Sort in descending order as that's the order they're shown on the boot screen
82+
// After this:
83+
// all_configs[0] -> booted depl
84+
// all_configs[1] -> rollback depl
85+
let mut all_configs = get_sorted_bls_boot_entries(&boot_dir, false)?;
86+
87+
// Update the indicies so that they're swapped
88+
for (idx, cfg) in all_configs.iter_mut().enumerate() {
89+
cfg.sort_key = Some(idx.to_string());
90+
}
91+
92+
// TODO(Johan-Liebert): Currently assuming there are only two deployments
93+
assert!(all_configs.len() == 2);
94+
95+
// Write these
96+
let dir_path = PathBuf::from(format!(
97+
"/sysroot/boot/loader/{ROLLBACK_BOOT_LOADER_ENTRIES}",
98+
));
99+
create_dir_all(&dir_path).with_context(|| format!("Failed to create dir: {dir_path:?}"))?;
100+
101+
let rollback_entries_dir =
102+
cap_std::fs::Dir::open_ambient_dir(&dir_path, cap_std::ambient_authority())
103+
.with_context(|| format!("Opening {dir_path:?}"))?;
104+
105+
// Write the BLS configs in there
106+
for cfg in all_configs {
107+
// SAFETY: We set sort_key above
108+
let file_name = format!("bootc-composefs-{}.conf", cfg.sort_key.as_ref().unwrap());
109+
110+
rollback_entries_dir
111+
.atomic_write(&file_name, cfg.to_string())
112+
.with_context(|| format!("Writing to {file_name}"))?;
113+
}
114+
115+
// Should we sync after every write?
116+
fsync(
117+
rollback_entries_dir
118+
.reopen_as_ownedfd()
119+
.with_context(|| format!("Reopening {dir_path:?} as owned fd"))?,
120+
)
121+
.with_context(|| format!("fsync {dir_path:?}"))?;
122+
123+
// Atomically exchange "entries" <-> "entries.rollback"
124+
let dir = Dir::open_ambient_dir("/sysroot/boot/loader", cap_std::ambient_authority())
125+
.context("Opening loader dir")?;
126+
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(())
151+
}
152+
153+
#[context("Rolling back composefs")]
154+
pub(crate) async fn composefs_rollback() -> Result<()> {
155+
let host = composefs_deployment_status().await?;
156+
157+
let new_spec = {
158+
let mut new_spec = host.spec.clone();
159+
new_spec.boot_order = new_spec.boot_order.swap();
160+
new_spec
161+
};
162+
163+
// Just to be sure
164+
host.spec.verify_transition(&new_spec)?;
165+
166+
let reverting = new_spec.boot_order == BootOrder::Default;
167+
if reverting {
168+
println!("notice: Reverting queued rollback state");
169+
}
170+
171+
let rollback_status = host
172+
.status
173+
.rollback
174+
.ok_or_else(|| anyhow!("No rollback available"))?;
175+
176+
// TODO: Handle staged deployment
177+
// Ostree will drop any staged deployment on rollback but will keep it if it is the first item
178+
// in the new deployment list
179+
let Some(rollback_composefs_entry) = &rollback_status.composefs else {
180+
anyhow::bail!("Rollback deployment not a composefs deployment")
181+
};
182+
183+
match rollback_composefs_entry.boot_type {
184+
BootType::Bls => rollback_composefs_bls(),
185+
BootType::Uki => rollback_composefs_uki(),
186+
}?;
187+
188+
if reverting {
189+
println!("Next boot: current deployment");
190+
} else {
191+
println!("Next boot: rollback deployment");
192+
}
193+
194+
Ok(())
195+
}

0 commit comments

Comments
 (0)