1
0
mirror of https://github.com/containers/bootc.git synced 2026-02-05 06:45:13 +01:00

Merge pull request #1314 from Johan-Liebert1/composefs-backend

Composefs-native backend
This commit is contained in:
Colin Walters
2025-08-01 14:53:05 -04:00
committed by GitHub
16 changed files with 1294 additions and 369 deletions

48
Cargo.lock generated
View File

@@ -245,8 +245,7 @@ dependencies = [
"liboverdrop",
"libsystemd",
"linkme",
"openat",
"openat-ext",
"nom 8.0.0",
"openssl",
"ostree-ext",
"regex",
@@ -1409,7 +1408,7 @@ dependencies = [
"libc",
"log",
"nix 0.27.1",
"nom",
"nom 7.1.3",
"once_cell",
"serde",
"sha2",
@@ -1581,19 +1580,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "nix"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c"
dependencies = [
"bitflags 1.3.2",
"cc",
"cfg-if",
"libc",
"memoffset 0.6.5",
]
[[package]]
name = "nix"
version = "0.25.1"
@@ -1630,6 +1616,15 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "nom"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
dependencies = [
"memchr",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
@@ -1707,27 +1702,6 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "openat"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95aa7c05907b3ebde2610d602f4ddd992145cc6a84493647c30396f30ba83abe"
dependencies = [
"libc",
]
[[package]]
name = "openat-ext"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cf3e4baa7f516441f58373f58aaf6e91a5dfa2e2b50e68a0d313b082014c61d"
dependencies = [
"libc",
"nix 0.23.2",
"openat",
"rand 0.8.5",
]
[[package]]
name = "openssh-keys"
version = "0.6.4"

View File

@@ -56,8 +56,7 @@ tini = "1.3.0"
comfy-table = "7.1.1"
thiserror = { workspace = true }
canon-json = { workspace = true }
openat = "0.1.21"
openat-ext = "0.2.3"
nom = "8.0.0"
[dev-dependencies]
similar-asserts = { workspace = true }

View File

@@ -1,88 +0,0 @@
use serde::{Deserialize, Deserializer};
use serde::de::Error;
use std::collections::HashMap;
use anyhow::Result;
#[derive(Debug, Deserialize, Eq)]
pub(crate) struct BLSConfig {
pub(crate) title: Option<String>,
#[serde(deserialize_with = "deserialize_version")]
pub(crate) version: u32,
pub(crate) linux: String,
pub(crate) initrd: String,
pub(crate) options: String,
#[serde(flatten)]
pub(crate) extra: HashMap<String, String>,
}
impl PartialEq for BLSConfig {
fn eq(&self, other: &Self) -> bool {
self.version == other.version
}
}
impl PartialOrd for BLSConfig {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.version.partial_cmp(&other.version)
}
}
impl Ord for BLSConfig {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.version.cmp(&other.version)
}
}
impl BLSConfig {
pub(crate) fn to_string(&self) -> String {
let mut out = String::new();
if let Some(title) = &self.title {
out += &format!("title {}\n", title);
}
out += &format!("version {}\n", self.version);
out += &format!("linux {}\n", self.linux);
out += &format!("initrd {}\n", self.initrd);
out += &format!("options {}\n", self.options);
for (key, value) in &self.extra {
out += &format!("{} {}\n", key, value);
}
out
}
}
fn deserialize_version<'de, D>(deserializer: D) -> Result<u32, D::Error>
where
D: Deserializer<'de>,
{
let s: Option<String> = Option::deserialize(deserializer)?;
match s {
Some(s) => Ok(s.parse::<u32>().map_err(D::Error::custom)?),
None => Err(D::Error::custom("Version not found")),
}
}
pub(crate) fn parse_bls_config(input: &str) -> Result<BLSConfig> {
let mut map = HashMap::new();
for line in input.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once(' ') {
map.insert(key.to_string(), value.trim().to_string());
}
}
let value = serde_json::to_value(map)?;
let parsed: BLSConfig = serde_json::from_value(value)?;
Ok(parsed)
}

View File

@@ -20,7 +20,7 @@ use ostree_container::store::PrepareResult;
use ostree_ext::composefs::fsverity;
use ostree_ext::composefs::fsverity::FsVerityHashValue;
use ostree_ext::container as ostree_container;
use ostree_ext::container_utils::{composefs_booted, ostree_booted};
use ostree_ext::container_utils::ostree_booted;
use ostree_ext::keyfileext::KeyFileExt;
use ostree_ext::ostree;
use schemars::schema_for;
@@ -36,7 +36,7 @@ use crate::progress_jsonl::{ProgressWriter, RawProgressFd};
use crate::spec::Host;
use crate::spec::ImageReference;
use crate::status::composefs_deployment_status;
use crate::utils::sigpolicy_from_opt;
use crate::utils::{composefs_booted, sigpolicy_from_opt};
/// Shared progress options
#[derive(Debug, Parser, PartialEq, Eq)]
@@ -798,13 +798,29 @@ async fn upgrade_composefs(_opts: UpgradeOpts) -> Result<()> {
};
let boot_type = BootType::from(&entry);
let mut boot_digest = None;
match boot_type {
BootType::Bls => setup_composefs_bls_boot(BootSetupType::Upgrade, repo, &id, entry),
BootType::Uki => setup_composefs_uki_boot(BootSetupType::Upgrade, repo, &id, entry),
}?;
BootType::Bls => {
boot_digest = Some(setup_composefs_bls_boot(
BootSetupType::Upgrade,
repo,
&id,
entry,
)?)
}
write_composefs_state(&Utf8PathBuf::from("/sysroot"), id, imgref, true, boot_type)?;
BootType::Uki => setup_composefs_uki_boot(BootSetupType::Upgrade, repo, &id, entry)?,
};
write_composefs_state(
&Utf8PathBuf::from("/sysroot"),
id,
imgref,
true,
boot_type,
boot_digest,
)?;
Ok(())
}
@@ -966,11 +982,19 @@ async fn switch_composefs(opts: SwitchOpts) -> Result<()> {
};
let boot_type = BootType::from(&entry);
let mut boot_digest = None;
match boot_type {
BootType::Bls => setup_composefs_bls_boot(BootSetupType::Upgrade, repo, &id, entry),
BootType::Uki => setup_composefs_uki_boot(BootSetupType::Upgrade, repo, &id, entry),
}?;
BootType::Bls => {
boot_digest = Some(setup_composefs_bls_boot(
BootSetupType::Upgrade,
repo,
&id,
entry,
)?)
}
BootType::Uki => setup_composefs_uki_boot(BootSetupType::Upgrade, repo, &id, entry)?,
};
write_composefs_state(
&Utf8PathBuf::from("/sysroot"),
@@ -978,6 +1002,7 @@ async fn switch_composefs(opts: SwitchOpts) -> Result<()> {
&target_imgref,
true,
boot_type,
boot_digest,
)?;
Ok(())

View File

@@ -0,0 +1,38 @@
/// composefs= paramter in kernel cmdline
pub const COMPOSEFS_CMDLINE: &str = "composefs=";
/// composefs=? paramter in kernel cmdline. The `?` signifies that the fs-verity validation is
/// optional in case the filesystem doesn't support it.
pub const COMPOSEFS_INSECURE_CMDLINE: &str = "composefs=?";
/// Directory to store transient state, such as staged deployemnts etc
pub(crate) const COMPOSEFS_TRANSIENT_STATE_DIR: &str = "/run/composefs";
/// File created in /run/composefs to record a staged-deployment
pub(crate) const COMPOSEFS_STAGED_DEPLOYMENT_FNAME: &str = "staged-deployment";
/// Absolute path to composefs-native state directory
pub(crate) const STATE_DIR_ABS: &str = "/sysroot/state/deploy";
/// Relative path to composefs-native state directory. Relative to /sysroot
pub(crate) const STATE_DIR_RELATIVE: &str = "state/deploy";
/// Relative path to the shared 'var' directory. Relative to /sysroot
pub(crate) const SHARED_VAR_PATH: &str = "state/os/default/var";
/// Section in .origin file to store boot related metadata
pub(crate) const ORIGIN_KEY_BOOT: &str = "boot";
/// Whether the deployment was booted with BLS or UKI
pub(crate) const ORIGIN_KEY_BOOT_TYPE: &str = "boot_type";
/// Key to store the SHA256 sum of vmlinuz + initrd for a deployment
pub(crate) const ORIGIN_KEY_BOOT_DIGEST: &str = "digest";
/// Filename for `loader/entries`
pub(crate) const BOOT_LOADER_ENTRIES: &str = "entries";
/// Filename for staged boot loader entries
pub(crate) const STAGED_BOOT_LOADER_ENTRIES: &str = "entries.staged";
/// Filename for rollback boot loader entries
pub(crate) const ROLLBACK_BOOT_LOADER_ENTRIES: &str = STAGED_BOOT_LOADER_ENTRIES;
/// Filename for grub user config
pub(crate) const USER_CFG: &str = "user.cfg";
/// Filename for staged grub user config
pub(crate) const USER_CFG_STAGED: &str = "user.cfg.staged";
/// Filename for rollback grub user config
pub(crate) const USER_CFG_ROLLBACK: &str = USER_CFG_STAGED;

View File

@@ -3,8 +3,9 @@
//! Create a merged filesystem tree with the image and mounted configmaps.
use std::collections::HashSet;
use std::fmt::Write as _;
use std::fs::create_dir_all;
use std::io::{BufRead, Write};
use std::io::{BufRead, Read, Write};
use std::path::PathBuf;
use anyhow::Ok;
@@ -22,18 +23,21 @@ use ostree_ext::ostree::Deployment;
use ostree_ext::ostree::{self, Sysroot};
use ostree_ext::sysroot::SysrootLock;
use ostree_ext::tokio_util::spawn_blocking_cancellable_flatten;
use rustix::fs::{fsync, renameat_with, AtFlags, RenameFlags};
use crate::bls_config::{parse_bls_config, BLSConfig};
use crate::install::{get_efi_uuid_source, get_user_config, BootType};
use crate::composefs_consts::{
BOOT_LOADER_ENTRIES, ROLLBACK_BOOT_LOADER_ENTRIES, USER_CFG, USER_CFG_ROLLBACK,
};
use crate::install::{get_efi_uuid_source, BootType};
use crate::parsers::bls_config::{parse_bls_config, BLSConfig};
use crate::parsers::grub_menuconfig::{parse_grub_menuentry_file, MenuEntry};
use crate::progress_jsonl::{Event, ProgressWriter, SubTaskBytes, SubTaskStep};
use crate::spec::ImageReference;
use crate::spec::{BootOrder, HostSpec, BootEntry};
use crate::spec::{BootOrder, HostSpec};
use crate::status::{composefs_deployment_status, labels_of_config};
use crate::store::Storage;
use crate::utils::async_task_with_spinner;
use openat_ext::OpenatDirExt;
// TODO use https://github.com/ostreedev/ostree-rs-ext/pull/493/commits/afc1837ff383681b947de30c0cefc70080a4f87a
const BASE_IMAGE_PREFIX: &str = "ostree/container/baseimage/bootc";
@@ -743,52 +747,81 @@ pub(crate) async fn stage(
Ok(())
}
#[context("Rolling back UKI")]
pub(crate) fn rollback_composefs_uki(current: &BootEntry, rollback: &BootEntry) -> Result<()> {
let user_cfg_name = "grub2/user.cfg.staged";
let user_cfg_path = PathBuf::from("/sysroot/boot").join(user_cfg_name);
pub(crate) fn rollback_composefs_uki() -> Result<()> {
let user_cfg_path = PathBuf::from("/sysroot/boot/grub2");
let efi_uuid_source = get_efi_uuid_source();
let mut str = String::new();
let boot_dir =
cap_std::fs::Dir::open_ambient_dir("/sysroot/boot", cap_std::ambient_authority())
.context("Opening boot dir")?;
let mut menuentries =
get_sorted_uki_boot_entries(&boot_dir, &mut str).context("Getting UKI boot entries")?;
// TODO: Need to check if user.cfg.staged exists
let mut usr_cfg = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(user_cfg_path)
.with_context(|| format!("Opening {user_cfg_name}"))?;
// TODO(Johan-Liebert): Currently assuming there are only two deployments
assert!(menuentries.len() == 2);
usr_cfg.write(efi_uuid_source.as_bytes())?;
let (first, second) = menuentries.split_at_mut(1);
std::mem::swap(&mut first[0], &mut second[0]);
let verity = if let Some(composefs) = &rollback.composefs {
composefs.verity.clone()
} else {
// Shouldn't really happen
anyhow::bail!("Verity not found for rollback deployment")
};
usr_cfg.write(get_user_config(&verity).as_bytes())?;
let mut buffer = get_efi_uuid_source();
let verity = if let Some(composefs) = &current.composefs {
composefs.verity.clone()
} else {
// Shouldn't really happen
anyhow::bail!("Verity not found for booted deployment")
};
usr_cfg.write(get_user_config(&verity).as_bytes())?;
for entry in menuentries {
write!(buffer, "{entry}")?;
}
let entries_dir =
cap_std::fs::Dir::open_ambient_dir(&user_cfg_path, cap_std::ambient_authority())
.with_context(|| format!("Opening {user_cfg_path:?}"))?;
entries_dir
.atomic_write(USER_CFG_ROLLBACK, buffer)
.with_context(|| format!("Writing to {USER_CFG_ROLLBACK}"))?;
tracing::debug!("Atomically exchanging for {USER_CFG_ROLLBACK} and {USER_CFG}");
renameat_with(
&entries_dir,
USER_CFG_ROLLBACK,
&entries_dir,
USER_CFG,
RenameFlags::EXCHANGE,
)
.context("renameat")?;
tracing::debug!("Removing {USER_CFG_ROLLBACK}");
rustix::fs::unlinkat(&entries_dir, USER_CFG_ROLLBACK, AtFlags::empty()).context("unlinkat")?;
tracing::debug!("Syncing to disk");
fsync(
entries_dir
.reopen_as_ownedfd()
.with_context(|| format!("Reopening {user_cfg_path:?} as owned fd"))?,
)
.with_context(|| format!("fsync {user_cfg_path:?}"))?;
Ok(())
}
/// Filename for `loader/entries`
const CURRENT_ENTRIES: &str = "entries";
const ROLLBACK_ENTRIES: &str = "entries.staged";
// Need str to store lifetime
pub(crate) fn get_sorted_uki_boot_entries<'a>(
boot_dir: &Dir,
str: &'a mut String,
) -> Result<Vec<MenuEntry<'a>>> {
let mut file = boot_dir
.open(format!("grub2/{USER_CFG}"))
.with_context(|| format!("Opening {USER_CFG}"))?;
file.read_to_string(str)?;
parse_grub_menuentry_file(str)
}
#[context("Getting boot entries")]
pub(crate) fn get_sorted_boot_entries(ascending: bool) -> Result<Vec<BLSConfig>> {
#[context("Getting sorted BLS entries")]
pub(crate) fn get_sorted_bls_boot_entries(
boot_dir: &Dir,
ascending: bool,
) -> Result<Vec<BLSConfig>> {
let mut all_configs = vec![];
for entry in std::fs::read_dir(format!("/sysroot/boot/loader/{CURRENT_ENTRIES}"))? {
for entry in boot_dir.read_dir(format!("loader/{BOOT_LOADER_ENTRIES}"))? {
let entry = entry?;
let file_name = entry.file_name();
@@ -801,8 +834,13 @@ pub(crate) fn get_sorted_boot_entries(ascending: bool) -> Result<Vec<BLSConfig>>
continue;
}
let contents = std::fs::read_to_string(&entry.path())
.with_context(|| format!("Failed to read {:?}", entry.path()))?;
let mut file = entry
.open()
.with_context(|| format!("Failed to open {:?}", file_name))?;
let mut contents = String::new();
file.read_to_string(&mut contents)
.with_context(|| format!("Failed to read {:?}", file_name))?;
let config = parse_bls_config(&contents).context("Parsing bls config")?;
@@ -816,50 +854,77 @@ pub(crate) fn get_sorted_boot_entries(ascending: bool) -> Result<Vec<BLSConfig>>
#[context("Rolling back BLS")]
pub(crate) fn rollback_composefs_bls() -> Result<()> {
let boot_dir =
cap_std::fs::Dir::open_ambient_dir("/sysroot/boot", cap_std::ambient_authority())
.context("Opening boot dir")?;
// Sort in descending order as that's the order they're shown on the boot screen
// After this:
// all_configs[0] -> booted depl
// all_configs[1] -> rollback depl
let mut all_configs = get_sorted_boot_entries(false)?;
let mut all_configs = get_sorted_bls_boot_entries(&boot_dir, false)?;
// Update the indicies so that they're swapped
for (idx, cfg) in all_configs.iter_mut().enumerate() {
cfg.version = idx as u32;
}
// TODO(Johan-Liebert): Currently assuming there are only two deployments
assert!(all_configs.len() == 2);
// Write these
let dir_path = PathBuf::from(format!("/sysroot/boot/loader/{ROLLBACK_ENTRIES}"));
let dir_path = PathBuf::from(format!(
"/sysroot/boot/loader/{ROLLBACK_BOOT_LOADER_ENTRIES}"
));
create_dir_all(&dir_path).with_context(|| format!("Failed to create dir: {dir_path:?}"))?;
let rollback_entries_dir =
cap_std::fs::Dir::open_ambient_dir(&dir_path, cap_std::ambient_authority())
.with_context(|| format!("Opening {dir_path:?}"))?;
// Write the BLS configs in there
for cfg in all_configs {
let file_name = format!("bootc-composefs-{}.conf", cfg.version);
let mut file = std::fs::OpenOptions::new()
.create(true)
.write(true)
.open(dir_path.join(&file_name))
.with_context(|| format!("Opening {file_name}"))?;
file.write_all(cfg.to_string().as_bytes())
rollback_entries_dir
.atomic_write(&file_name, cfg.to_string())
.with_context(|| format!("Writing to {file_name}"))?;
}
// Should we sync after every write?
fsync(
rollback_entries_dir
.reopen_as_ownedfd()
.with_context(|| format!("Reopening {dir_path:?} as owned fd"))?,
)
.with_context(|| format!("fsync {dir_path:?}"))?;
// Atomically exchange "entries" <-> "entries.rollback"
let dir = openat::Dir::open("/sysroot/boot/loader").context("Opening loader dir")?;
let dir = Dir::open_ambient_dir("/sysroot/boot/loader", cap_std::ambient_authority())
.context("Opening loader dir")?;
tracing::debug!("Atomically exchanging for {ROLLBACK_ENTRIES} and {CURRENT_ENTRIES}");
dir.local_exchange(ROLLBACK_ENTRIES, CURRENT_ENTRIES)
.context("local exchange")?;
tracing::debug!(
"Atomically exchanging for {ROLLBACK_BOOT_LOADER_ENTRIES} and {BOOT_LOADER_ENTRIES}"
);
renameat_with(
&dir,
ROLLBACK_BOOT_LOADER_ENTRIES,
&dir,
BOOT_LOADER_ENTRIES,
RenameFlags::EXCHANGE,
)
.context("renameat")?;
tracing::debug!("Removing {ROLLBACK_ENTRIES}");
dir.remove_all(ROLLBACK_ENTRIES)
.context("Removing entries.rollback")?;
tracing::debug!("Removing {ROLLBACK_BOOT_LOADER_ENTRIES}");
rustix::fs::unlinkat(&dir, ROLLBACK_BOOT_LOADER_ENTRIES, AtFlags::empty())
.context("unlinkat")?;
tracing::debug!("Syncing to disk");
dir.syncfs().context("syncfs")?;
fsync(
dir.reopen_as_ownedfd()
.with_context(|| format!("Reopening /sysroot/boot/loader as owned fd"))?,
)
.context("fsync")?;
Ok(())
}
@@ -896,9 +961,15 @@ pub(crate) async fn composefs_rollback() -> Result<()> {
match rollback_composefs_entry.boot_type {
BootType::Bls => rollback_composefs_bls(),
BootType::Uki => rollback_composefs_uki(&host.status.booted.unwrap(), &rollback_status),
BootType::Uki => rollback_composefs_uki(),
}?;
if reverting {
println!("Next boot: current deployment");
} else {
println!("Next boot: rollback deployment");
}
Ok(())
}
@@ -1111,6 +1182,10 @@ pub(crate) fn fixup_etc_fstab(root: &Dir) -> Result<()> {
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use crate::parsers::grub_menuconfig::MenuentryBody;
use super::*;
#[test]
@@ -1205,4 +1280,119 @@ UUID=6907-17CA /boot/efi vfat umask=0077,shortname=win
assert_eq!(tempdir.read_to_string("etc/fstab")?, modified);
Ok(())
}
#[test]
fn test_sorted_bls_boot_entries() -> Result<()> {
let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
let entry1 = r#"
title Fedora 42.20250623.3.1 (CoreOS)
version 1
linux /boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10
initrd /boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img
options root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6
"#;
let entry2 = r#"
title Fedora 41.20250214.2.0 (CoreOS)
version 2
linux /boot/febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01/vmlinuz-5.14.10
initrd /boot/febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01/initramfs-5.14.10.img
options root=UUID=abc123 rw composefs=febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01
"#;
tempdir.create_dir_all("loader/entries")?;
tempdir.atomic_write(
"loader/entries/random_file.txt",
"Random file that we won't parse",
)?;
tempdir.atomic_write("loader/entries/entry1.conf", entry1)?;
tempdir.atomic_write("loader/entries/entry2.conf", entry2)?;
let result = get_sorted_bls_boot_entries(&tempdir, true);
let mut expected = vec![
BLSConfig {
title: Some("Fedora 42.20250623.3.1 (CoreOS)".into()),
version: 1,
linux: "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10".into(),
initrd: "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img".into(),
options: "root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6".into(),
extra: HashMap::new(),
},
BLSConfig {
title: Some("Fedora 41.20250214.2.0 (CoreOS)".into()),
version: 2,
linux: "/boot/febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01/vmlinuz-5.14.10".into(),
initrd: "/boot/febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01/initramfs-5.14.10.img".into(),
options: "root=UUID=abc123 rw composefs=febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01".into(),
extra: HashMap::new(),
},
];
assert_eq!(result.unwrap(), expected);
let result = get_sorted_bls_boot_entries(&tempdir, false);
expected.reverse();
assert_eq!(result.unwrap(), expected);
Ok(())
}
#[test]
fn test_sorted_uki_boot_entries() -> Result<()> {
let user_cfg = r#"
if [ -f ${config_directory}/efiuuid.cfg ]; then
source ${config_directory}/efiuuid.cfg
fi
menuentry "Fedora Bootc UKI: (f7415d75017a12a387a39d2281e033a288fc15775108250ef70a01dcadb93346)" {
insmod fat
insmod chain
search --no-floppy --set=root --fs-uuid "${EFI_PART_UUID}"
chainloader /EFI/Linux/f7415d75017a12a387a39d2281e033a288fc15775108250ef70a01dcadb93346.efi
}
menuentry "Fedora Bootc UKI: (7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6)" {
insmod fat
insmod chain
search --no-floppy --set=root --fs-uuid "${EFI_PART_UUID}"
chainloader /EFI/Linux/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6.efi
}
"#;
let bootdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
bootdir.create_dir_all(format!("grub2"))?;
bootdir.atomic_write(format!("grub2/{USER_CFG}"), user_cfg)?;
let mut s = String::new();
let result = get_sorted_uki_boot_entries(&bootdir, &mut s)?;
let expected = vec![
MenuEntry {
title: "Fedora Bootc UKI: (f7415d75017a12a387a39d2281e033a288fc15775108250ef70a01dcadb93346)".into(),
body: MenuentryBody {
insmod: vec!["fat", "chain"],
chainloader: "/EFI/Linux/f7415d75017a12a387a39d2281e033a288fc15775108250ef70a01dcadb93346.efi".into(),
search: "--no-floppy --set=root --fs-uuid \"${EFI_PART_UUID}\"",
version: 0,
extra: vec![],
},
},
MenuEntry {
title: "Fedora Bootc UKI: (7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6)".into(),
body: MenuentryBody {
insmod: vec!["fat", "chain"],
chainloader: "/EFI/Linux/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6.efi".into(),
search: "--no-floppy --set=root --fs-uuid \"${EFI_PART_UUID}\"",
version: 0,
extra: vec![],
},
},
];
assert_eq!(result, expected);
Ok(())
}
}

View File

@@ -49,9 +49,9 @@ use ostree_ext::composefs::{
repository::Repository as ComposefsRepository,
util::Sha256Digest,
};
use ostree_ext::composefs_boot::bootloader::UsrLibModulesVmlinuz;
use ostree_ext::composefs_boot::{
bootloader::BootEntry as ComposefsBootEntry,
write_boot::write_boot_simple as composefs_write_boot_simple, BootOps,
bootloader::BootEntry as ComposefsBootEntry, cmdline::get_cmdline_composefs, uki, BootOps,
};
use ostree_ext::composefs_oci::{
image::create_filesystem as create_composefs_filesystem, pull as composefs_oci_pull,
@@ -69,21 +69,31 @@ use ostree_ext::{
use rustix::fs::FileTypeExt;
use rustix::fs::MetadataExt as _;
use rustix::path::Arg;
use serde::{Deserialize, Serialize};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[cfg(feature = "install-to-disk")]
use self::baseline::InstallBlockDeviceOpts;
use crate::bls_config::{parse_bls_config, BLSConfig};
use crate::boundimage::{BoundImage, ResolvedBoundImage};
use crate::composefs_consts::{
BOOT_LOADER_ENTRIES, COMPOSEFS_CMDLINE, COMPOSEFS_INSECURE_CMDLINE,
COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR, ORIGIN_KEY_BOOT,
ORIGIN_KEY_BOOT_DIGEST, ORIGIN_KEY_BOOT_TYPE, SHARED_VAR_PATH, STAGED_BOOT_LOADER_ENTRIES,
STATE_DIR_ABS, STATE_DIR_RELATIVE, USER_CFG, USER_CFG_STAGED,
};
use crate::containerenv::ContainerExecutionInfo;
use crate::deploy::{prepare_for_pull, pull_from_prepared, PreparedImportMeta, PreparedPullResult};
use crate::deploy::{
get_sorted_uki_boot_entries, prepare_for_pull, pull_from_prepared, PreparedImportMeta,
PreparedPullResult,
};
use crate::lsm;
use crate::parsers::bls_config::{parse_bls_config, BLSConfig};
use crate::parsers::grub_menuconfig::MenuEntry;
use crate::progress_jsonl::ProgressWriter;
use crate::spec::ImageReference;
use crate::store::Storage;
use crate::task::Task;
use crate::utils::sigpolicy_from_opt;
use crate::utils::{path_relative_to, sigpolicy_from_opt};
use bootc_mount::{inspect_filesystem, Filesystem};
/// The toplevel boot directory
@@ -269,7 +279,9 @@ impl TryFrom<&str> for BootType {
match value {
"bls" => Ok(Self::Bls),
"uki" => Ok(Self::Uki),
unrecognized => Err(anyhow::anyhow!("Unrecognized boot option: '{unrecognized}'")),
unrecognized => Err(anyhow::anyhow!(
"Unrecognized boot option: '{unrecognized}'"
)),
}
}
}
@@ -287,8 +299,9 @@ impl From<&ComposefsBootEntry<Sha256HashValue>> for BootType {
#[derive(Debug, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)]
pub(crate) struct InstallComposefsOpts {
#[clap(long, value_enum, default_value_t)]
pub(crate) boot: BootType,
#[clap(long, default_value_t)]
#[serde(default)]
pub(crate) insecure: bool,
}
#[cfg(feature = "install-to-disk")]
@@ -316,9 +329,11 @@ pub(crate) struct InstallToDiskOpts {
pub(crate) via_loopback: bool,
#[clap(long)]
#[serde(default)]
pub(crate) composefs_native: bool,
#[clap(flatten)]
#[serde(flatten)]
pub(crate) composefs_opts: InstallComposefsOpts,
}
@@ -607,17 +622,12 @@ impl FromStr for MountSpec {
impl InstallToDiskOpts {
pub(crate) fn validate(&self) -> Result<()> {
if !self.composefs_native {
// Reject using --boot without --composefs
if self.composefs_opts.boot != BootType::default() {
anyhow::bail!("--boot must not be provided without --composefs");
// Reject using --insecure without --composefs
if self.composefs_opts.insecure != false {
anyhow::bail!("--insecure must not be provided without --composefs");
}
}
// Can't add kargs to UKI
if self.composefs_opts.boot == BootType::Uki && self.config_opts.karg.is_some() {
anyhow::bail!("Cannot pass kargs to UKI");
}
Ok(())
}
}
@@ -1533,7 +1543,7 @@ async fn initialize_composefs_repository(
rootfs_dir
.create_dir_all("composefs")
.context("Creating dir 'composefs'")?;
.context("Creating dir composefs")?;
let repo = open_composefs_repo(rootfs_dir)?;
@@ -1548,7 +1558,10 @@ async fn initialize_composefs_repository(
fn get_booted_bls() -> Result<BLSConfig> {
let cmdline = crate::kernel::parse_cmdline()?;
let booted = cmdline.iter().find_map(|x| x.strip_prefix("composefs="));
let booted = cmdline.iter().find_map(|x| {
x.strip_prefix(COMPOSEFS_INSECURE_CMDLINE)
.or_else(|| x.strip_prefix(COMPOSEFS_CMDLINE))
});
let Some(booted) = booted else {
anyhow::bail!("Failed to find composefs parameter in kernel cmdline");
@@ -1591,11 +1604,138 @@ pub fn read_file<ObjectID: FsVerityHashValue>(
pub(crate) enum BootSetupType<'a> {
/// For initial setup, i.e. install to-disk
Setup(&'a RootSetup),
Setup((&'a RootSetup, &'a State)),
/// For `bootc upgrade`
Upgrade,
}
/// Compute SHA256Sum of VMlinuz + Initrd
///
/// # Arguments
/// * entry - BootEntry containing VMlinuz and Initrd
/// * repo - The composefs repository
#[context("Computing boot digest")]
fn compute_boot_digest(
entry: &UsrLibModulesVmlinuz<Sha256HashValue>,
repo: &ComposefsRepository<Sha256HashValue>,
) -> Result<String> {
let vmlinuz = read_file(&entry.vmlinuz, &repo).context("Reading vmlinuz")?;
let Some(initramfs) = &entry.initramfs else {
anyhow::bail!("initramfs not found");
};
let initramfs = read_file(initramfs, &repo).context("Reading intird")?;
let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256())
.context("Creating hasher")?;
hasher.update(&vmlinuz).context("hashing vmlinuz")?;
hasher.update(&initramfs).context("hashing initrd")?;
let digest: &[u8] = &hasher.finish().context("Finishing digest")?;
return Ok(hex::encode(digest));
}
/// Given the SHA256 sum of current VMlinuz + Initrd combo, find boot entry with the same SHA256Sum
///
/// # Returns
/// Returns the verity of the deployment that has a boot digest same as the one passed in
#[context("Checking boot entry duplicates")]
fn find_vmlinuz_initrd_duplicates(digest: &str) -> Result<Option<String>> {
let deployments =
cap_std::fs::Dir::open_ambient_dir(STATE_DIR_ABS, cap_std::ambient_authority());
let deployments = match deployments {
Ok(d) => d,
// The first ever deployment
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => anyhow::bail!(e),
};
let mut symlink_to: Option<String> = None;
for depl in deployments.entries()? {
let depl = depl?;
let depl_file_name = depl.file_name();
let depl_file_name = depl_file_name.as_str()?;
let config = depl
.open_dir()
.with_context(|| format!("Opening {depl_file_name}"))?
.read_to_string(format!("{depl_file_name}.origin"))
.context("Reading origin file")?;
let ini = tini::Ini::from_string(&config)
.with_context(|| format!("Failed to parse file {depl_file_name}.origin as ini"))?;
match ini.get::<String>(ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_DIGEST) {
Some(hash) => {
if hash == digest {
symlink_to = Some(depl_file_name.to_string());
break;
}
}
// No SHASum recorded in origin file
// `symlink_to` is already none, but being explicit here
None => symlink_to = None,
};
}
Ok(symlink_to)
}
#[context("Writing BLS entries to disk")]
fn write_bls_boot_entries_to_disk(
boot_dir: &Utf8PathBuf,
deployment_id: &Sha256HashValue,
entry: &UsrLibModulesVmlinuz<Sha256HashValue>,
repo: &ComposefsRepository<Sha256HashValue>,
) -> Result<()> {
let id_hex = deployment_id.to_hex();
// Write the initrd and vmlinuz at /boot/<id>/
let path = boot_dir.join(&id_hex);
create_dir_all(&path)?;
let entries_dir = cap_std::fs::Dir::open_ambient_dir(&path, cap_std::ambient_authority())
.with_context(|| format!("Opening {path}"))?;
entries_dir
.atomic_write(
"vmlinuz",
read_file(&entry.vmlinuz, &repo).context("Reading vmlinuz")?,
)
.context("Writing vmlinuz to path")?;
let Some(initramfs) = &entry.initramfs else {
anyhow::bail!("initramfs not found");
};
entries_dir
.atomic_write(
"initrd",
read_file(initramfs, &repo).context("Reading initrd")?,
)
.context("Writing initrd to path")?;
// Can't call fsync on O_PATH fds, so re-open it as a non O_PATH fd
let owned_fd = entries_dir
.reopen_as_ownedfd()
.context("Reopen as owned fd")?;
rustix::fs::fsync(owned_fd).context("fsync")?;
Ok(())
}
/// Sets up and writes BLS entries and binaries (VMLinuz + Initrd) to disk
///
/// # Returns
/// Returns the SHA256Sum of VMLinuz + Initrd combo. Error if any
#[context("Setting up BLS boot")]
pub(crate) fn setup_composefs_bls_boot(
setup_type: BootSetupType,
@@ -1603,14 +1743,22 @@ pub(crate) fn setup_composefs_bls_boot(
repo: ComposefsRepository<Sha256HashValue>,
id: &Sha256HashValue,
entry: ComposefsBootEntry<Sha256HashValue>,
) -> Result<()> {
) -> Result<String> {
let id_hex = id.to_hex();
let (root_path, cmdline_refs) = match setup_type {
BootSetupType::Setup(root_setup) => {
BootSetupType::Setup((root_setup, state)) => {
// root_setup.kargs has [root=UUID=<UUID>, "rw"]
let mut cmdline_options = String::from(root_setup.kargs.join(" "));
cmdline_options.push_str(&format!(" composefs={id_hex}"));
match &state.composefs_options {
Some(opt) if opt.insecure => {
cmdline_options.push_str(&format!(" {COMPOSEFS_INSECURE_CMDLINE}{id_hex}"));
}
None | Some(..) => {
cmdline_options.push_str(&format!(" {COMPOSEFS_CMDLINE}{id_hex}"));
}
};
(root_setup.physical_root_path.clone(), cmdline_options)
}
@@ -1620,7 +1768,7 @@ pub(crate) fn setup_composefs_bls_boot(
vec![
format!("root=UUID={DPS_UUID}"),
RW_KARG.to_string(),
format!("composefs={id_hex}"),
format!("{COMPOSEFS_CMDLINE}{id_hex}"),
]
.join(" "),
),
@@ -1628,71 +1776,74 @@ pub(crate) fn setup_composefs_bls_boot(
let boot_dir = root_path.join("boot");
let bls_config = match &entry {
let is_upgrade = matches!(setup_type, BootSetupType::Upgrade);
let (bls_config, boot_digest) = match &entry {
ComposefsBootEntry::Type1(..) => todo!(),
ComposefsBootEntry::Type2(..) => todo!(),
ComposefsBootEntry::UsrLibModulesUki(..) => todo!(),
ComposefsBootEntry::UsrLibModulesVmLinuz(usr_lib_modules_vmlinuz) => {
// Write the initrd and vmlinuz at /boot/<id>/
let path = boot_dir.join(&id_hex);
create_dir_all(&path)?;
let boot_digest = compute_boot_digest(usr_lib_modules_vmlinuz, &repo)
.context("Computing boot digest")?;
let vmlinuz_path = path.join("vmlinuz");
let initrd_path = path.join("initrd");
std::fs::write(
&vmlinuz_path,
read_file(&usr_lib_modules_vmlinuz.vmlinuz, &repo).context("Reading vmlinuz")?,
)
.context("Writing vmlinuz to path")?;
if let Some(initramfs) = &usr_lib_modules_vmlinuz.initramfs {
std::fs::write(
&initrd_path,
read_file(initramfs, &repo).context("Reading initramfs")?,
)
.context("Writing initrd to path")?;
} else {
anyhow::bail!("initramfs not found");
};
BLSConfig {
let mut bls_config = BLSConfig {
title: Some(id_hex.clone()),
version: 1,
linux: format!("/boot/{id_hex}/vmlinuz"),
initrd: format!("/boot/{id_hex}/initrd"),
options: cmdline_refs,
extra: HashMap::new(),
};
if let Some(symlink_to) = find_vmlinuz_initrd_duplicates(&boot_digest)? {
bls_config.linux = format!("/boot/{symlink_to}/vmlinuz");
bls_config.initrd = format!("/boot/{symlink_to}/initrd");
} else {
write_bls_boot_entries_to_disk(&boot_dir, id, usr_lib_modules_vmlinuz, &repo)?;
}
(bls_config, boot_digest)
}
};
let (entries_path, booted_bls) = if matches!(setup_type, BootSetupType::Upgrade) {
let (entries_path, booted_bls) = if is_upgrade {
let mut booted_bls = get_booted_bls()?;
booted_bls.version = 0; // entries are sorted by their filename in reverse order
// This will be atomically renamed to 'loader/entries' on shutdown/reboot
(boot_dir.join("loader/entries.staged"), Some(booted_bls))
(
boot_dir.join(format!("loader/{STAGED_BOOT_LOADER_ENTRIES}")),
Some(booted_bls),
)
} else {
(boot_dir.join("loader/entries"), None)
(boot_dir.join(format!("loader/{BOOT_LOADER_ENTRIES}")), None)
};
create_dir_all(&entries_path).with_context(|| format!("Creating {:?}", entries_path))?;
std::fs::write(
entries_path.join(format!("bootc-composefs-{}.conf", bls_config.version)),
let loader_entries_dir =
cap_std::fs::Dir::open_ambient_dir(&entries_path, cap_std::ambient_authority())
.with_context(|| format!("Opening {entries_path}"))?;
loader_entries_dir.atomic_write(
format!("bootc-composefs-{}.conf", bls_config.version),
bls_config.to_string().as_bytes(),
)?;
if let Some(booted_bls) = booted_bls {
std::fs::write(
entries_path.join(format!("bootc-composefs-{}.conf", booted_bls.version)),
loader_entries_dir.atomic_write(
format!("bootc-composefs-{}.conf", booted_bls.version),
booted_bls.to_string().as_bytes(),
)?;
}
Ok(())
let owned_loader_entries_fd = loader_entries_dir
.reopen_as_ownedfd()
.context("Reopening as owned fd")?;
rustix::fs::fsync(owned_loader_entries_fd).context("fsync")?;
Ok(boot_digest)
}
pub fn get_esp_partition(device: &str) -> Result<(String, Option<String>)> {
@@ -1706,21 +1857,6 @@ pub fn get_esp_partition(device: &str) -> Result<(String, Option<String>)> {
Ok((esp.node, esp.uuid))
}
pub(crate) fn get_user_config(uki_id: &str) -> String {
let s = format!(
r#"
menuentry "Fedora Bootc UKI: ({uki_id})" {{
insmod fat
insmod chain
search --no-floppy --set=root --fs-uuid "${{EFI_PART_UUID}}"
chainloader /EFI/Linux/{uki_id}.efi
}}
"#
);
return s;
}
/// Contains the EFP's filesystem UUID. Used by grub
pub(crate) const EFI_UUID_FILE: &str = "efiuuid.cfg";
@@ -1744,8 +1880,14 @@ pub(crate) fn setup_composefs_uki_boot(
id: &Sha256HashValue,
entry: ComposefsBootEntry<Sha256HashValue>,
) -> Result<()> {
let (root_path, esp_device) = match setup_type {
BootSetupType::Setup(root_setup) => {
let (root_path, esp_device, is_insecure_from_opts) = match setup_type {
BootSetupType::Setup((root_setup, state)) => {
if let Some(v) = &state.config_opts.karg {
if v.len() > 0 {
tracing::warn!("kargs passed for UKI will be ignored");
}
}
let esp_part = root_setup
.device_info
.partitions
@@ -1753,7 +1895,11 @@ pub(crate) fn setup_composefs_uki_boot(
.find(|p| p.parttype.as_str() == ESP_GUID)
.ok_or_else(|| anyhow!("ESP partition not found"))?;
(root_setup.physical_root_path.clone(), esp_part.node.clone())
(
root_setup.physical_root_path.clone(),
esp_part.node.clone(),
state.composefs_options.as_ref().map(|x| x.insecure),
)
}
BootSetupType::Upgrade => {
@@ -1766,7 +1912,7 @@ pub(crate) fn setup_composefs_uki_boot(
anyhow::bail!("Could not find parent device for mountpoint /sysroot");
};
(sysroot, get_esp_partition(&parent)?.0)
(sysroot, get_esp_partition(&parent)?.0, None)
}
};
@@ -1779,16 +1925,66 @@ pub(crate) fn setup_composefs_uki_boot(
.args([&PathBuf::from(&esp_device), &mounted_esp.clone()])
.run()?;
composefs_write_boot_simple(
&repo,
entry,
&id,
false,
&mounted_esp,
None,
Some(&id.to_hex()),
&[],
)?;
let boot_label = match entry {
ComposefsBootEntry::Type1(..) => todo!(),
ComposefsBootEntry::UsrLibModulesUki(..) => todo!(),
ComposefsBootEntry::UsrLibModulesVmLinuz(..) => todo!(),
ComposefsBootEntry::Type2(type2_entry) => {
let uki = read_file(&type2_entry.file, &repo).context("Reading UKI")?;
let cmdline = uki::get_cmdline(&uki).context("Getting UKI cmdline")?;
let (composefs_cmdline, insecure) = get_cmdline_composefs::<Sha256HashValue>(cmdline)?;
// If the UKI cmdline does not match what the user has passed as cmdline option
// NOTE: This will only be checked for new installs and now upgrades/switches
if let Some(is_insecure_from_opts) = is_insecure_from_opts {
match is_insecure_from_opts {
true => {
if !insecure {
tracing::warn!(
"--insecure passed as option but UKI cmdline does not support it"
)
}
}
false => {
if insecure {
tracing::warn!("UKI cmdline has composefs set as insecure")
}
}
}
}
let boot_label = uki::get_boot_label(&uki).context("Getting UKI boot label")?;
if composefs_cmdline != *id {
anyhow::bail!(
"The UKI has the wrong composefs= parameter (is '{composefs_cmdline:?}', should be {id:?})"
);
}
// Write the UKI to ESP
let efi_linux_path = mounted_esp.join("EFI/Linux");
create_dir_all(&efi_linux_path).context("Creating EFI/Linux")?;
let efi_linux =
cap_std::fs::Dir::open_ambient_dir(&efi_linux_path, cap_std::ambient_authority())
.with_context(|| format!("Opening {efi_linux_path:?}"))?;
efi_linux
.atomic_write(format!("{}.efi", id.to_hex()), uki)
.context("Writing UKI")?;
rustix::fs::fsync(
efi_linux
.reopen_as_ownedfd()
.context("Reopening as owned fd")?,
)
.context("fsync")?;
boot_label
}
};
Task::new("Unmounting ESP", "umount")
.arg(&mounted_esp)
@@ -1809,66 +2005,75 @@ pub(crate) fn setup_composefs_uki_boot(
let efi_uuid_source = get_efi_uuid_source();
let user_cfg_name = if is_upgrade {
"grub2/user.cfg.staged"
USER_CFG_STAGED
} else {
"grub2/user.cfg"
USER_CFG
};
let user_cfg_path = boot_dir.join(user_cfg_name);
let grub_dir =
cap_std::fs::Dir::open_ambient_dir(boot_dir.join("grub2"), cap_std::ambient_authority())
.context("opening boot/grub2")?;
// Iterate over all available deployments, and generate a menuentry for each
//
// TODO: We might find a staged deployment here
if is_upgrade {
let mut usr_cfg = std::fs::OpenOptions::new()
.write(true)
.create(true)
.open(user_cfg_path)
.with_context(|| format!("Opening {user_cfg_name}"))?;
let mut buffer = vec![];
usr_cfg.write_all(efi_uuid_source.as_bytes())?;
usr_cfg.write_all(get_user_config(&id.to_hex()).as_bytes())?;
// Shouldn't really fail so no context here
buffer.write_all(efi_uuid_source.as_bytes())?;
buffer.write_all(
MenuEntry::new(&boot_label, &id.to_hex())
.to_string()
.as_bytes(),
)?;
// root_path here will be /sysroot
for entry in std::fs::read_dir(root_path.join(STATE_DIR_RELATIVE))? {
let entry = entry?;
let mut str_buf = String::new();
let boot_dir = cap_std::fs::Dir::open_ambient_dir(boot_dir, cap_std::ambient_authority())
.context("Opening boot dir")?;
let entries = get_sorted_uki_boot_entries(&boot_dir, &mut str_buf)?;
let depl_file_name = entry.file_name();
// SAFETY: Deployment file name shouldn't containg non UTF-8 chars
let depl_file_name = depl_file_name.to_string_lossy();
// Write out only the currently booted entry, which should be the very first one
// Even if we have booted into the second menuentry "boot entry", the default will be the
// first one
buffer.write_all(entries[0].to_string().as_bytes())?;
usr_cfg.write_all(get_user_config(&depl_file_name).as_bytes())?;
}
grub_dir
.atomic_write(user_cfg_name, buffer)
.with_context(|| format!("Writing to {user_cfg_name}"))?;
rustix::fs::fsync(grub_dir.reopen_as_ownedfd()?).context("fsync")?;
return Ok(());
}
let efi_uuid_file_path = format!("grub2/{EFI_UUID_FILE}");
// Open grub2/efiuuid.cfg and write the EFI partition fs-UUID in there
// This will be sourced by grub2/user.cfg to be used for `--fs-uuid`
let mut efi_uuid_file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.open(boot_dir.join(&efi_uuid_file_path))
.with_context(|| format!("Opening {efi_uuid_file_path}"))?;
let esp_uuid = Task::new("blkid for ESP UUID", "blkid")
.args(["-s", "UUID", "-o", "value", &esp_device])
.read()?;
efi_uuid_file
.write_all(format!("set EFI_PART_UUID=\"{}\"", esp_uuid.trim()).as_bytes())
.with_context(|| format!("Writing to {efi_uuid_file_path}"))?;
grub_dir.atomic_write(
EFI_UUID_FILE,
format!("set EFI_PART_UUID=\"{}\"", esp_uuid.trim()).as_bytes(),
)?;
// Write to grub2/user.cfg
let mut usr_cfg = std::fs::OpenOptions::new()
.write(true)
.create(true)
.open(user_cfg_path)
.with_context(|| format!("Opening {user_cfg_name}"))?;
let mut buffer = vec![];
usr_cfg.write_all(efi_uuid_source.as_bytes())?;
usr_cfg.write_all(get_user_config(&id.to_hex()).as_bytes())?;
// Shouldn't really fail so no context here
buffer.write_all(efi_uuid_source.as_bytes())?;
buffer.write_all(
MenuEntry::new(&boot_label, &id.to_hex())
.to_string()
.as_bytes(),
)?;
grub_dir
.atomic_write(user_cfg_name, buffer)
.with_context(|| format!("Writing to {user_cfg_name}"))?;
rustix::fs::fsync(grub_dir.reopen_as_ownedfd()?).context("fsync")?;
Ok(())
}
@@ -1937,17 +2142,26 @@ fn setup_composefs_boot(root_setup: &RootSetup, state: &State, image_id: &str) -
anyhow::bail!("No boot entries!");
};
let Some(composefs_opts) = &state.composefs_options else {
anyhow::bail!("Could not find options for composefs")
};
let boot_type = BootType::from(&entry);
let mut boot_digest: Option<String> = None;
match composefs_opts.boot {
match boot_type {
BootType::Bls => {
setup_composefs_bls_boot(BootSetupType::Setup(&root_setup), repo, &id, entry)?
}
BootType::Uki => {
setup_composefs_uki_boot(BootSetupType::Setup(&root_setup), repo, &id, entry)?
let digest = setup_composefs_bls_boot(
BootSetupType::Setup((&root_setup, &state)),
repo,
&id,
entry,
)?;
boot_digest = Some(digest);
}
BootType::Uki => setup_composefs_uki_boot(
BootSetupType::Setup((&root_setup, &state)),
repo,
&id,
entry,
)?,
};
write_composefs_state(
@@ -1959,20 +2173,13 @@ fn setup_composefs_boot(root_setup: &RootSetup, state: &State, image_id: &str) -
signature: None,
},
false,
composefs_opts.boot,
boot_type,
boot_digest,
)?;
Ok(())
}
pub(crate) const COMPOSEFS_TRANSIENT_STATE_DIR: &str = "/run/composefs";
pub(crate) const COMPOSEFS_STAGED_DEPLOYMENT_PATH: &str = "/run/composefs/staged-deployment";
/// Relative to /sysroot
pub(crate) const STATE_DIR_RELATIVE: &str = "state/deploy";
pub(crate) const ORIGIN_KEY_BOOT: &str = "boot";
pub(crate) const ORIGIN_KEY_BOOT_TYPE: &str = "boot_type";
/// Creates and populates /sysroot/state/deploy/image_id
#[context("Writing composefs state")]
pub(crate) fn write_composefs_state(
@@ -1981,17 +2188,22 @@ pub(crate) fn write_composefs_state(
imgref: &ImageReference,
staged: bool,
boot_type: BootType,
boot_digest: Option<String>,
) -> Result<()> {
let state_path = root_path.join(format!("{STATE_DIR_RELATIVE}/{}", deployment_id.to_hex()));
create_dir_all(state_path.join("etc/upper"))?;
create_dir_all(state_path.join("etc/work"))?;
let actual_var_path = root_path.join(format!("state/os/fedora/var"));
let actual_var_path = root_path.join(SHARED_VAR_PATH);
create_dir_all(&actual_var_path)?;
symlink(Path::new("../../os/fedora/var"), state_path.join("var"))
.context("Failed to create symlink for /var")?;
symlink(
path_relative_to(state_path.as_std_path(), actual_var_path.as_std_path())
.context("Getting var symlink path")?,
state_path.join("var"),
)
.context("Failed to create symlink for /var")?;
let ImageReference {
image: image_name,
@@ -2008,25 +2220,38 @@ pub(crate) fn write_composefs_state(
.section(ORIGIN_KEY_BOOT)
.item(ORIGIN_KEY_BOOT_TYPE, boot_type);
let mut origin_file =
std::fs::File::create(state_path.join(format!("{}.origin", deployment_id.to_hex())))
.context("Failed to open .origin file")?;
if let Some(boot_digest) = boot_digest {
config = config
.section(ORIGIN_KEY_BOOT)
.item(ORIGIN_KEY_BOOT_DIGEST, boot_digest);
}
origin_file
.write(config.to_string().as_bytes())
let state_dir = cap_std::fs::Dir::open_ambient_dir(&state_path, cap_std::ambient_authority())
.context("Opening state dir")?;
state_dir
.atomic_write(
format!("{}.origin", deployment_id.to_hex()),
config.to_string().as_bytes(),
)
.context("Falied to write to .origin file")?;
if staged {
std::fs::create_dir_all(COMPOSEFS_TRANSIENT_STATE_DIR)
.with_context(|| format!("Creating {COMPOSEFS_TRANSIENT_STATE_DIR}"))?;
let mut file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.open(COMPOSEFS_STAGED_DEPLOYMENT_PATH)
.context("Opening staged-deployment file")?;
let staged_depl_dir = cap_std::fs::Dir::open_ambient_dir(
COMPOSEFS_TRANSIENT_STATE_DIR,
cap_std::ambient_authority(),
)
.with_context(|| format!("Opening {COMPOSEFS_TRANSIENT_STATE_DIR}"))?;
file.write_all(deployment_id.to_hex().as_bytes())?;
staged_depl_dir
.atomic_write(
COMPOSEFS_STAGED_DEPLOYMENT_FNAME,
deployment_id.to_hex().as_bytes(),
)
.with_context(|| format!("Writing to {COMPOSEFS_STAGED_DEPLOYMENT_FNAME}"))?;
}
Ok(())

View File

@@ -427,6 +427,7 @@ pub(crate) fn install_create_rootfs(
.flatten()
.chain([rootarg, RW_KARG.to_string()].into_iter())
.chain(bootarg)
.chain(state.config_opts.karg.clone().into_iter().flatten())
.collect::<Vec<_>>();
bootc_mount::mount(&rootdev, &physical_root_path)?;

View File

@@ -6,6 +6,7 @@
mod boundimage;
pub mod cli;
mod composefs_consts;
pub(crate) mod deploy;
pub(crate) mod fsck;
pub(crate) mod generator;
@@ -18,6 +19,7 @@ pub(crate) mod kargs;
mod lints;
mod lsm;
pub(crate) mod metadata;
pub(crate) mod parsers;
mod podman;
mod progress_jsonl;
mod reboot;
@@ -27,7 +29,6 @@ mod status;
mod store;
mod task;
mod utils;
mod bls_config;
#[cfg(feature = "docgen")]
mod docgen;

View File

@@ -0,0 +1,201 @@
use anyhow::Result;
use serde::de::Error;
use serde::{Deserialize, Deserializer};
use std::collections::HashMap;
use std::fmt::Display;
#[derive(Debug, Deserialize, Eq)]
pub(crate) struct BLSConfig {
pub(crate) title: Option<String>,
#[serde(deserialize_with = "deserialize_version")]
pub(crate) version: u32,
pub(crate) linux: String,
pub(crate) initrd: String,
pub(crate) options: String,
#[serde(flatten)]
pub(crate) extra: HashMap<String, String>,
}
impl PartialEq for BLSConfig {
fn eq(&self, other: &Self) -> bool {
self.version == other.version
}
}
impl PartialOrd for BLSConfig {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.version.partial_cmp(&other.version)
}
}
impl Ord for BLSConfig {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.version.cmp(&other.version)
}
}
impl Display for BLSConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(title) = &self.title {
writeln!(f, "title {}", title)?;
}
writeln!(f, "version {}", self.version)?;
writeln!(f, "linux {}", self.linux)?;
writeln!(f, "initrd {}", self.initrd)?;
writeln!(f, "options {}", self.options)?;
for (key, value) in &self.extra {
writeln!(f, "{} {}", key, value)?;
}
Ok(())
}
}
fn deserialize_version<'de, D>(deserializer: D) -> Result<u32, D::Error>
where
D: Deserializer<'de>,
{
let s: Option<String> = Option::deserialize(deserializer)?;
match s {
Some(s) => Ok(s.parse::<u32>().map_err(D::Error::custom)?),
None => Err(D::Error::custom("Version not found")),
}
}
pub(crate) fn parse_bls_config(input: &str) -> Result<BLSConfig> {
let mut map = HashMap::new();
for line in input.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once(' ') {
map.insert(key.to_string(), value.trim().to_string());
}
}
let value = serde_json::to_value(map)?;
let parsed: BLSConfig = serde_json::from_value(value)?;
Ok(parsed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_valid_bls_config() -> Result<()> {
let input = r#"
title Fedora 42.20250623.3.1 (CoreOS)
version 2
linux /boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10
initrd /boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img
options root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6
custom1 value1
custom2 value2
"#;
let config = parse_bls_config(input)?;
assert_eq!(
config.title,
Some("Fedora 42.20250623.3.1 (CoreOS)".to_string())
);
assert_eq!(config.version, 2);
assert_eq!(config.linux, "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10");
assert_eq!(config.initrd, "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img");
assert_eq!(config.options, "root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6");
assert_eq!(config.extra.get("custom1"), Some(&"value1".to_string()));
assert_eq!(config.extra.get("custom2"), Some(&"value2".to_string()));
Ok(())
}
#[test]
fn test_parse_missing_version() {
let input = r#"
title Fedora
linux /vmlinuz
initrd /initramfs.img
options root=UUID=xyz ro quiet
"#;
let parsed = parse_bls_config(input);
assert!(parsed.is_err());
}
#[test]
fn test_parse_invalid_version_format() {
let input = r#"
title Fedora
version not_an_int
linux /vmlinuz
initrd /initramfs.img
options root=UUID=abc composefs=some-uuid
"#;
let parsed = parse_bls_config(input);
assert!(parsed.is_err());
}
#[test]
fn test_display_output() -> Result<()> {
let input = r#"
title Test OS
version 10
linux /boot/vmlinuz
initrd /boot/initrd.img
options root=UUID=abc composefs=some-uuid
foo bar
"#;
let config = parse_bls_config(input)?;
let output = format!("{}", config);
let mut output_lines = output.lines();
assert_eq!(output_lines.next().unwrap(), "title Test OS");
assert_eq!(output_lines.next().unwrap(), "version 10");
assert_eq!(output_lines.next().unwrap(), "linux /boot/vmlinuz");
assert_eq!(output_lines.next().unwrap(), "initrd /boot/initrd.img");
assert_eq!(
output_lines.next().unwrap(),
"options root=UUID=abc composefs=some-uuid"
);
assert_eq!(output_lines.next().unwrap(), "foo bar");
Ok(())
}
#[test]
fn test_ordering() -> Result<()> {
let config1 = parse_bls_config(
r#"
title Entry 1
version 3
linux /vmlinuz-3
initrd /initrd-3
options opt1
"#,
)?;
let config2 = parse_bls_config(
r#"
title Entry 2
version 5
linux /vmlinuz-5
initrd /initrd-5
options opt2
"#,
)?;
assert!(config1 < config2);
Ok(())
}
}

View File

@@ -0,0 +1,269 @@
use std::fmt::Display;
use nom::{
bytes::complete::{tag, take_until},
character::complete::multispace0,
error::{Error, ErrorKind, ParseError},
multi::many0,
sequence::{delimited, preceded},
Err, IResult, Parser,
};
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct MenuentryBody<'a> {
pub(crate) insmod: Vec<&'a str>,
pub(crate) chainloader: String,
pub(crate) search: &'a str,
pub(crate) version: u8,
pub(crate) extra: Vec<(&'a str, &'a str)>,
}
impl<'a> Display for MenuentryBody<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for insmod in &self.insmod {
writeln!(f, "insmod {}", insmod)?;
}
writeln!(f, "search {}", self.search)?;
// writeln!(f, "version {}", self.version)?;
writeln!(f, "chainloader {}", self.chainloader)?;
for (k, v) in &self.extra {
writeln!(f, "{k} {v}")?;
}
Ok(())
}
}
impl<'a> From<Vec<(&'a str, &'a str)>> for MenuentryBody<'a> {
fn from(vec: Vec<(&'a str, &'a str)>) -> Self {
let mut entry = Self {
insmod: vec![],
chainloader: "".into(),
search: "",
version: 0,
extra: vec![],
};
for (key, value) in vec {
match key {
"insmod" => entry.insmod.push(value),
"chainloader" => entry.chainloader = value.into(),
"search" => entry.search = value,
"set" => {}
_ => entry.extra.push((key, value)),
}
}
return entry;
}
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct MenuEntry<'a> {
pub(crate) title: String,
pub(crate) body: MenuentryBody<'a>,
}
impl<'a> Display for MenuEntry<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "menuentry \"{}\" {{", self.title)?;
write!(f, "{}", self.body)?;
writeln!(f, "}}")
}
}
impl<'a> MenuEntry<'a> {
pub(crate) fn new(boot_label: &str, uki_id: &str) -> Self {
Self {
title: format!("{boot_label}: ({uki_id})"),
body: MenuentryBody {
insmod: vec!["fat", "chain"],
chainloader: format!("/EFI/Linux/{uki_id}.efi"),
search: "--no-floppy --set=root --fs-uuid \"${EFI_PART_UUID}\"",
version: 0,
extra: vec![],
},
}
}
}
pub fn take_until_balanced_allow_nested(
opening_bracket: char,
closing_bracket: char,
) -> impl Fn(&str) -> IResult<&str, &str> {
move |i: &str| {
let mut index = 0;
let mut bracket_counter = 0;
while let Some(n) = &i[index..].find(&[opening_bracket, closing_bracket, '\\'][..]) {
index += n;
let mut characters = i[index..].chars();
match characters.next().unwrap_or_default() {
c if c == '\\' => {
// Skip '\'
index += '\\'.len_utf8();
// Skip char following '\'
let c = characters.next().unwrap_or_default();
index += c.len_utf8();
}
c if c == opening_bracket => {
bracket_counter += 1;
index += opening_bracket.len_utf8();
}
c if c == closing_bracket => {
bracket_counter -= 1;
index += closing_bracket.len_utf8();
}
// Should not happen
_ => unreachable!(),
};
// We found the unmatched closing bracket.
if bracket_counter == -1 {
// Don't consume it as we'll "tag" it afterwards
index -= closing_bracket.len_utf8();
return Ok((&i[index..], &i[0..index]));
};
}
if bracket_counter == 0 {
Ok(("", i))
} else {
Err(Err::Error(Error::from_error_kind(i, ErrorKind::TakeUntil)))
}
}
}
fn parse_menuentry(input: &str) -> IResult<&str, MenuEntry> {
let (input, _) = take_until("menuentry")(input)?; // skip irrelevant prefix
let (input, _) = tag("menuentry").parse(input)?;
// Skip the whitespace after "menuentry"
let (input, _) = multispace0.parse(input)?;
// Eat up the title
let (input, title) = delimited(tag("\""), take_until("\""), tag("\"")).parse(input)?;
// Skip any whitespace after title
let (input, _) = multispace0.parse(input)?;
// Eat up everything insde { .. }
let (input, body) = delimited(
tag("{"),
take_until_balanced_allow_nested('{', '}'),
tag("}"),
)
.parse(input)?;
let mut map = vec![];
for line in body.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once(' ') {
map.push((key, value.trim()));
}
}
Ok((
input,
MenuEntry {
title: title.to_string(),
body: MenuentryBody::from(map),
},
))
}
#[rustfmt::skip]
fn parse_all(input: &str) -> IResult<&str, Vec<MenuEntry>> {
many0(
preceded(
multispace0,
parse_menuentry,
)
)
.parse(input)
}
pub(crate) fn parse_grub_menuentry_file(contents: &str) -> anyhow::Result<Vec<MenuEntry>> {
let result = parse_all(&contents);
return match result {
Ok((_, entries)) => Ok(entries),
Result::Err(_) => anyhow::bail!("Failed to parse grub menuentry"),
};
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_menuconfig_parser() {
let menuentry = r#"
if [ -f ${config_directory}/efiuuid.cfg ]; then
source ${config_directory}/efiuuid.cfg
fi
# Skip this comment
menuentry "Fedora 42: (Verity-42)" {
insmod fat
insmod chain
# This should also be skipped
search --no-floppy --set=root --fs-uuid "${EFI_PART_UUID}"
chainloader /EFI/Linux/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6.efi
}
menuentry "Fedora 43: (Verity-43)" {
insmod fat
insmod chain
search --no-floppy --set=root --fs-uuid "${EFI_PART_UUID}"
chainloader /EFI/Linux/uki.efi
extra_field1 this is extra
extra_field2 this is also extra
}
"#;
let result = parse_grub_menuentry_file(menuentry).expect("Expected parsed entries");
let expected = vec![
MenuEntry {
title: "Fedora 42: (Verity-42)".into(),
body: MenuentryBody {
insmod: vec!["fat", "chain"],
search: "--no-floppy --set=root --fs-uuid \"${EFI_PART_UUID}\"",
chainloader: "/EFI/Linux/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6.efi".into(),
version: 0,
extra: vec![],
},
},
MenuEntry {
title: "Fedora 43: (Verity-43)".into(),
body: MenuentryBody {
insmod: vec!["fat", "chain"],
search: "--no-floppy --set=root --fs-uuid \"${EFI_PART_UUID}\"",
chainloader: "/EFI/Linux/uki.efi".into(),
version: 0,
extra: vec![
("extra_field1", "this is extra"),
("extra_field2", "this is also extra")
]
},
},
];
println!("{}", expected[0]);
assert_eq!(result, expected);
}
}

View File

@@ -0,0 +1,2 @@
pub(crate) mod bls_config;
pub(crate) mod grub_menuconfig;

View File

@@ -165,7 +165,6 @@ pub struct BootEntryOstree {
pub deploy_serial: u32,
}
/// A bootable entry
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "camelCase")]

View File

@@ -14,7 +14,6 @@ use ostree::glib;
use ostree_container::OstreeImageReference;
use ostree_ext::container as ostree_container;
use ostree_ext::container::deploy::ORIGIN_CONTAINER;
use ostree_ext::container_utils::composefs_booted;
use ostree_ext::container_utils::ostree_booted;
use ostree_ext::containers_image_proxy;
use ostree_ext::keyfileext::KeyFileExt;
@@ -24,15 +23,18 @@ use ostree_ext::ostree;
use tokio::io::AsyncReadExt;
use crate::cli::OutputFormat;
use crate::deploy::get_sorted_boot_entries;
use crate::composefs_consts::{
COMPOSEFS_CMDLINE, COMPOSEFS_INSECURE_CMDLINE, COMPOSEFS_STAGED_DEPLOYMENT_FNAME,
COMPOSEFS_TRANSIENT_STATE_DIR, ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_TYPE, STATE_DIR_RELATIVE,
};
use crate::deploy::get_sorted_bls_boot_entries;
use crate::deploy::get_sorted_uki_boot_entries;
use crate::install::BootType;
use crate::install::ORIGIN_KEY_BOOT;
use crate::install::ORIGIN_KEY_BOOT_TYPE;
use crate::install::{COMPOSEFS_STAGED_DEPLOYMENT_PATH, STATE_DIR_RELATIVE};
use crate::spec::ImageStatus;
use crate::spec::{BootEntry, BootOrder, Host, HostSpec, HostStatus, HostType};
use crate::spec::{ImageReference, ImageSignature};
use crate::store::{CachedImageStatus, ContainerImageStore, Storage};
use crate::utils::composefs_booted;
impl From<ostree_container::SignatureSource> for ImageSignature {
fn from(sig: ostree_container::SignatureSource) -> Self {
@@ -392,7 +394,11 @@ async fn boot_entry_from_composefs_deployment(
#[context("Getting composefs deployment status")]
pub(crate) async fn composefs_deployment_status() -> Result<Host> {
let cmdline = crate::kernel::parse_cmdline()?;
let booted_image_verity = cmdline.iter().find_map(|x| x.strip_prefix("composefs="));
let booted_image_verity = cmdline.iter().find_map(|x| {
x.strip_prefix(COMPOSEFS_INSECURE_CMDLINE)
.or_else(|| x.strip_prefix(COMPOSEFS_CMDLINE))
});
let Some(booted_image_verity) = booted_image_verity else {
anyhow::bail!("Failed to find composefs parameter in kernel cmdline");
@@ -411,7 +417,9 @@ pub(crate) async fn composefs_deployment_status() -> Result<Host> {
let mut host = Host::new(host_spec);
let staged_deployment_id = match std::fs::File::open(COMPOSEFS_STAGED_DEPLOYMENT_PATH) {
let staged_deployment_id = match std::fs::File::open(format!(
"{COMPOSEFS_TRANSIENT_STATE_DIR}/{COMPOSEFS_STAGED_DEPLOYMENT_FNAME}"
)) {
Ok(mut f) => {
let mut s = String::new();
f.read_to_string(&mut s)?;
@@ -422,6 +430,9 @@ pub(crate) async fn composefs_deployment_status() -> Result<Host> {
Err(e) => Err(e),
}?;
// NOTE: This cannot work if we support both BLS and UKI at the same time
let mut boot_type: Option<BootType> = None;
for depl in deployments {
let depl = depl?;
@@ -441,6 +452,21 @@ pub(crate) async fn composefs_deployment_status() -> Result<Host> {
let boot_entry =
boot_entry_from_composefs_deployment(ini, depl_file_name.to_string()).await?;
// SAFETY: boot_entry.composefs will always be present
let boot_type_from_origin = boot_entry.composefs.as_ref().unwrap().boot_type;
match boot_type {
Some(current_type) => {
if current_type != boot_type_from_origin {
anyhow::bail!("Conflicting boot types")
}
}
None => {
boot_type = Some(boot_type_from_origin);
}
};
if depl.file_name() == booted_image_verity {
host.spec.image = boot_entry.image.as_ref().map(|x| x.image.clone());
host.status.booted = Some(boot_entry);
@@ -457,11 +483,33 @@ pub(crate) async fn composefs_deployment_status() -> Result<Host> {
host.status.rollback = Some(boot_entry);
}
host.status.rollback_queued = !get_sorted_boot_entries(false)?
.first()
.ok_or(anyhow::anyhow!("First boot entry not found"))?
.options
.contains(booted_image_verity);
// Shouldn't really happen, but for sanity nonetheless
let Some(boot_type) = boot_type else {
anyhow::bail!("Could not determine boot type");
};
let boot_dir = sysroot.open_dir("boot").context("Opening boot dir")?;
match boot_type {
BootType::Bls => {
host.status.rollback_queued = !get_sorted_bls_boot_entries(&boot_dir, false)?
.first()
.ok_or(anyhow::anyhow!("First boot entry not found"))?
.options
.contains(booted_image_verity);
}
BootType::Uki => {
let mut s = String::new();
host.status.rollback_queued = !get_sorted_uki_boot_entries(&boot_dir, &mut s)?
.first()
.ok_or(anyhow::anyhow!("First boot entry not found"))?
.body
.chainloader
.contains(booted_image_verity);
}
};
if host.status.rollback_queued {
host.spec.boot_order = BootOrder::Rollback

View File

@@ -1,8 +1,9 @@
use std::future::Future;
use std::io::Write;
use std::os::fd::BorrowedFd;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;
use std::{future::Future, path::Component};
use anyhow::{Context, Result};
use bootc_utils::CommandRunExt;
@@ -17,6 +18,14 @@ use ostree::glib;
use ostree_ext::container::SignatureSource;
use ostree_ext::ostree;
use crate::composefs_consts::{COMPOSEFS_CMDLINE, COMPOSEFS_INSECURE_CMDLINE};
/// Returns true if the system appears to have been booted with composefs without ostree.
pub fn composefs_booted() -> std::io::Result<bool> {
let cmdline = std::fs::read_to_string("/proc/cmdline")?;
Ok(cmdline.contains(COMPOSEFS_CMDLINE) || cmdline.contains(COMPOSEFS_INSECURE_CMDLINE))
}
/// Try to look for keys injected by e.g. rpm-ostree requesting machine-local
/// changes; if any are present, return `true`.
pub(crate) fn origin_has_rpmostree_stuff(kf: &glib::KeyFile) -> bool {
@@ -186,6 +195,28 @@ pub(crate) fn digested_pullspec(image: &str, digest: &str) -> String {
format!("{image}@{digest}")
}
/// Computes a relative path from `from` to `to`.
///
/// Both `from` and `to` must be absolute paths.
pub(crate) fn path_relative_to(from: &Path, to: &Path) -> Result<PathBuf> {
if !from.is_absolute() || !to.is_absolute() {
anyhow::bail!("Paths must be absolute");
}
let from = from.components().collect::<Vec<_>>();
let to = to.components().collect::<Vec<_>>();
let common = from.iter().zip(&to).take_while(|(a, b)| a == b).count();
let up = std::iter::repeat(Component::ParentDir).take(from.len() - common);
let mut final_path = PathBuf::new();
final_path.extend(up);
final_path.extend(&to[common..]);
return Ok(final_path);
}
#[cfg(test)]
mod tests {
use super::*;
@@ -223,4 +254,21 @@ mod tests {
SignatureSource::ContainerPolicyAllowInsecure
);
}
#[test]
fn test_relative_path() {
let from = Path::new("/sysroot/state/deploy/image_id");
let to = Path::new("/sysroot/state/os/default/var");
assert_eq!(
path_relative_to(from, to).unwrap(),
PathBuf::from("../../os/default/var")
);
assert_eq!(
path_relative_to(&Path::new("state/deploy"), to)
.unwrap_err()
.to_string(),
"Paths must be absolute"
);
}
}

View File

@@ -77,13 +77,6 @@ pub fn ostree_booted() -> io::Result<bool> {
Path::new(&format!("/{OSTREE_BOOTED}")).try_exists()
}
/// Returns true if the system appears to have been booted with composefs.
pub fn composefs_booted() -> io::Result<bool> {
let cmdline = std::fs::read_to_string("/proc/cmdline")?;
Ok(cmdline.contains("composefs="))
}
/// Returns true if the target root appears to have been booted via ostree.
pub fn is_ostree_booted_in(rootfs: &Dir) -> io::Result<bool> {
rootfs.try_exists(OSTREE_BOOTED)