mirror of
https://github.com/containers/bootc.git
synced 2026-02-05 06:45:13 +01:00
Add a composefs backend
This adds a new off-by default feature to enable a new composefs-native backend for bootc. This is all still a live work in progress, but we're landing this first tranche of work to help avoid continual issues with rebasing. Thanks to everyone who worked on it! xref https://github.com/bootc-dev/bootc/issues/1190 Co-authored-by: John Eckersberg <jeckersb@redhat.com> Co-authored-by: Robert Sturla <robertsturla@outlook.com> Co-authored-by: Colin Walters <walters@verbum.org> Signed-off-by: Pragyan Poudyal <pragyanpoudyal41999@gmail.com>
This commit is contained in:
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -255,6 +255,7 @@ dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"anyhow",
|
||||
"bootc-initramfs-setup",
|
||||
"bootc-internal-blockdev",
|
||||
"bootc-internal-utils",
|
||||
"bootc-kernel-cmdline",
|
||||
@@ -311,11 +312,13 @@ dependencies = [
|
||||
"anyhow",
|
||||
"bootc-internal-utils",
|
||||
"camino",
|
||||
"cap-std-ext",
|
||||
"fn-error-context",
|
||||
"indoc",
|
||||
"libc",
|
||||
"rustix 1.0.8",
|
||||
"serde",
|
||||
"tempfile",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
|
||||
@@ -20,7 +20,9 @@ use cap_std_ext::dirext::CapStdExtDirExt;
|
||||
use composefs::fsverity::{FsVerityHashValue, Sha256HashValue, Sha512HashValue};
|
||||
use composefs::generic_tree::{Directory, Inode, Leaf, LeafContent, Stat};
|
||||
use composefs::tree::ImageError;
|
||||
use rustix::fs::{AtFlags, Gid, Uid, XattrFlags, lgetxattr, llistxattr, lsetxattr, readlinkat};
|
||||
use rustix::fs::{
|
||||
AtFlags, Gid, Uid, XattrFlags, lgetxattr, llistxattr, lsetxattr, readlinkat, symlinkat,
|
||||
};
|
||||
|
||||
/// Metadata associated with a file, directory, or symlink entry.
|
||||
#[derive(Debug)]
|
||||
@@ -627,9 +629,8 @@ fn merge_leaf(
|
||||
.context(format!("Deleting {file:?}"))?;
|
||||
|
||||
if let Some(target) = symlink {
|
||||
new_etc_fd
|
||||
.symlink(target.as_ref(), &file)
|
||||
.context(format!("Creating symlink {file:?}"))?;
|
||||
// Using rustix's symlinkat here as we might have absolute symlinks which clash with ambient_authority
|
||||
symlinkat(&**target, new_etc_fd, file).context(format!("Creating symlink {file:?}"))?;
|
||||
} else {
|
||||
current_etc_fd
|
||||
.copy(&file, new_etc_fd, &file)
|
||||
|
||||
@@ -112,8 +112,9 @@ pub fn mount_at_wrapper(
|
||||
.with_context(|| format!("Mounting at path {path:?}"))
|
||||
}
|
||||
|
||||
/// Wrapper around [`rustix::openat`]
|
||||
#[context("Opening dir {name:?}")]
|
||||
fn open_dir(dirfd: impl AsFd, name: impl AsRef<Path> + Debug) -> Result<OwnedFd> {
|
||||
pub fn open_dir(dirfd: impl AsFd, name: impl AsRef<Path> + Debug) -> Result<OwnedFd> {
|
||||
let res = openat(
|
||||
dirfd,
|
||||
name.as_ref(),
|
||||
|
||||
@@ -12,3 +12,8 @@
|
||||
|
||||
pub mod bytes;
|
||||
pub mod utf8;
|
||||
|
||||
/// This is used by dracut.
|
||||
pub const INITRD_ARG_PREFIX: &str = "rd.";
|
||||
/// The kernel argument for configuring the rootfs flags.
|
||||
pub const ROOTFLAGS: &str = "rootflags";
|
||||
|
||||
@@ -23,6 +23,7 @@ bootc-tmpfiles = { path = "../tmpfiles" }
|
||||
bootc-utils = { package = "bootc-internal-utils", path = "../utils", version = "0.0.0" }
|
||||
ostree-ext = { path = "../ostree-ext", features = ["bootc"] }
|
||||
etc-merge = { path = "../etc-merge" }
|
||||
bootc-initramfs-setup = { path = "../initramfs" }
|
||||
|
||||
# Workspace dependencies
|
||||
anstream = { workspace = true }
|
||||
|
||||
793
crates/lib/src/bootc_composefs/boot.rs
Normal file
793
crates/lib/src/bootc_composefs/boot.rs
Normal file
@@ -0,0 +1,793 @@
|
||||
use std::fs::create_dir_all;
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
use std::{ffi::OsStr, path::PathBuf};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use bootc_blockdev::find_parent_devices;
|
||||
use bootc_mount::inspect_filesystem;
|
||||
use bootc_utils::CommandRunExt;
|
||||
use camino::{Utf8Path, Utf8PathBuf};
|
||||
use cap_std_ext::{cap_std, dirext::CapStdExtDirExt};
|
||||
use clap::ValueEnum;
|
||||
use composefs::fs::read_file;
|
||||
use composefs::tree::FileSystem;
|
||||
use composefs_boot::BootOps;
|
||||
use fn_error_context::context;
|
||||
use ostree_ext::composefs::{
|
||||
fsverity::{FsVerityHashValue, Sha256HashValue},
|
||||
repository::Repository as ComposefsRepository,
|
||||
};
|
||||
use ostree_ext::composefs_boot::bootloader::UsrLibModulesVmlinuz;
|
||||
use ostree_ext::composefs_boot::{
|
||||
bootloader::BootEntry as ComposefsBootEntry, cmdline::get_cmdline_composefs,
|
||||
os_release::OsReleaseInfo, uki,
|
||||
};
|
||||
use ostree_ext::composefs_oci::image::create_filesystem as create_composefs_filesystem;
|
||||
use rustix::path::Arg;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::bootc_composefs::repo::open_composefs_repo;
|
||||
use crate::bootc_composefs::state::{get_booted_bls, write_composefs_state};
|
||||
use crate::bootc_composefs::status::get_sorted_uki_boot_entries;
|
||||
use crate::parsers::bls_config::BLSConfig;
|
||||
use crate::parsers::grub_menuconfig::MenuEntry;
|
||||
use crate::spec::ImageReference;
|
||||
use crate::task::Task;
|
||||
use crate::{
|
||||
composefs_consts::{
|
||||
BOOT_LOADER_ENTRIES, COMPOSEFS_CMDLINE, ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_DIGEST,
|
||||
STAGED_BOOT_LOADER_ENTRIES, STATE_DIR_ABS, USER_CFG, USER_CFG_STAGED,
|
||||
},
|
||||
install::{DPS_UUID, ESP_GUID, RW_KARG},
|
||||
spec::{Bootloader, Host},
|
||||
};
|
||||
|
||||
use crate::install::{RootSetup, State};
|
||||
|
||||
/// Contains the EFP's filesystem UUID. Used by grub
|
||||
pub(crate) const EFI_UUID_FILE: &str = "efiuuid.cfg";
|
||||
/// The EFI Linux directory
|
||||
const EFI_LINUX: &str = "EFI/Linux";
|
||||
|
||||
pub(crate) enum BootSetupType<'a> {
|
||||
/// For initial setup, i.e. install to-disk
|
||||
Setup((&'a RootSetup, &'a State, &'a FileSystem<Sha256HashValue>)),
|
||||
/// For `bootc upgrade`
|
||||
Upgrade((&'a FileSystem<Sha256HashValue>, &'a Host)),
|
||||
}
|
||||
|
||||
#[derive(
|
||||
ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema,
|
||||
)]
|
||||
pub enum BootType {
|
||||
#[default]
|
||||
Bls,
|
||||
Uki,
|
||||
}
|
||||
|
||||
impl ::std::fmt::Display for BootType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let s = match self {
|
||||
BootType::Bls => "bls",
|
||||
BootType::Uki => "uki",
|
||||
};
|
||||
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for BootType {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
"bls" => Ok(Self::Bls),
|
||||
"uki" => Ok(Self::Uki),
|
||||
unrecognized => Err(anyhow::anyhow!(
|
||||
"Unrecognized boot option: '{unrecognized}'"
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ComposefsBootEntry<Sha256HashValue>> for BootType {
|
||||
fn from(entry: &ComposefsBootEntry<Sha256HashValue>) -> Self {
|
||||
match entry {
|
||||
ComposefsBootEntry::Type1(..) => Self::Bls,
|
||||
ComposefsBootEntry::Type2(..) => Self::Uki,
|
||||
ComposefsBootEntry::UsrLibModulesVmLinuz(..) => Self::Bls,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the beginning of the grub2/user.cfg file
|
||||
/// where we source a file containing the ESPs filesystem UUID
|
||||
pub(crate) fn get_efi_uuid_source() -> String {
|
||||
format!(
|
||||
r#"
|
||||
if [ -f ${{config_directory}}/{EFI_UUID_FILE} ]; then
|
||||
source ${{config_directory}}/{EFI_UUID_FILE}
|
||||
fi
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_esp_partition(device: &str) -> Result<(String, Option<String>)> {
|
||||
let device_info = bootc_blockdev::partitions_of(Utf8Path::new(device))?;
|
||||
let esp = device_info
|
||||
.partitions
|
||||
.into_iter()
|
||||
.find(|p| p.parttype.as_str() == ESP_GUID)
|
||||
.ok_or(anyhow::anyhow!("ESP not found for device: {device}"))?;
|
||||
|
||||
Ok((esp.node, esp.uuid))
|
||||
}
|
||||
|
||||
pub fn get_sysroot_parent_dev() -> Result<String> {
|
||||
let sysroot = Utf8PathBuf::from("/sysroot");
|
||||
|
||||
let fsinfo = inspect_filesystem(&sysroot)?;
|
||||
let parent_devices = find_parent_devices(&fsinfo.source)?;
|
||||
|
||||
let Some(parent) = parent_devices.into_iter().next() else {
|
||||
anyhow::bail!("Could not find parent device for mountpoint /sysroot");
|
||||
};
|
||||
|
||||
return Ok(parent);
|
||||
}
|
||||
|
||||
/// 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(())
|
||||
}
|
||||
|
||||
struct BLSEntryPath<'a> {
|
||||
/// Where to write vmlinuz/initrd
|
||||
entries_path: Utf8PathBuf,
|
||||
/// The absolute path, with reference to the partition's root, where the vmlinuz/initrd are written to
|
||||
/// We need this as when installing, the mounted path will not
|
||||
abs_entries_path: &'a str,
|
||||
/// Where to write the .conf files
|
||||
config_path: Utf8PathBuf,
|
||||
/// If we mounted EFI, the target path
|
||||
mount_path: Option<Utf8PathBuf>,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
// TODO: Make this generic
|
||||
repo: ComposefsRepository<Sha256HashValue>,
|
||||
id: &Sha256HashValue,
|
||||
entry: ComposefsBootEntry<Sha256HashValue>,
|
||||
) -> Result<String> {
|
||||
let id_hex = id.to_hex();
|
||||
|
||||
let (root_path, esp_device, cmdline_refs, fs, bootloader) = match setup_type {
|
||||
BootSetupType::Setup((root_setup, state, fs)) => {
|
||||
// root_setup.kargs has [root=UUID=<UUID>, "rw"]
|
||||
let mut cmdline_options = String::from(root_setup.kargs.join(" "));
|
||||
|
||||
match &state.composefs_options {
|
||||
Some(opt) if opt.insecure => {
|
||||
cmdline_options.push_str(&format!(" {COMPOSEFS_CMDLINE}=?{id_hex}"));
|
||||
}
|
||||
None | Some(..) => {
|
||||
cmdline_options.push_str(&format!(" {COMPOSEFS_CMDLINE}={id_hex}"));
|
||||
}
|
||||
};
|
||||
|
||||
// Locate ESP partition device
|
||||
let esp_part = root_setup
|
||||
.device_info
|
||||
.partitions
|
||||
.iter()
|
||||
.find(|p| p.parttype.as_str() == ESP_GUID)
|
||||
.ok_or_else(|| anyhow::anyhow!("ESP partition not found"))?;
|
||||
|
||||
(
|
||||
root_setup.physical_root_path.clone(),
|
||||
esp_part.node.clone(),
|
||||
cmdline_options,
|
||||
fs,
|
||||
state
|
||||
.composefs_options
|
||||
.as_ref()
|
||||
.map(|opts| opts.bootloader.clone())
|
||||
.unwrap_or(Bootloader::default()),
|
||||
)
|
||||
}
|
||||
|
||||
BootSetupType::Upgrade((fs, host)) => {
|
||||
let sysroot_parent = get_sysroot_parent_dev()?;
|
||||
let bootloader = host.require_composefs_booted()?.bootloader.clone();
|
||||
|
||||
(
|
||||
Utf8PathBuf::from("/sysroot"),
|
||||
get_esp_partition(&sysroot_parent)?.0,
|
||||
[
|
||||
format!("root=UUID={DPS_UUID}"),
|
||||
RW_KARG.to_string(),
|
||||
format!("{COMPOSEFS_CMDLINE}={id_hex}"),
|
||||
]
|
||||
.join(" "),
|
||||
fs,
|
||||
bootloader,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let is_upgrade = matches!(setup_type, BootSetupType::Upgrade(..));
|
||||
|
||||
let (entry_paths, _tmpdir_guard) = match bootloader {
|
||||
Bootloader::Grub => (
|
||||
BLSEntryPath {
|
||||
entries_path: root_path.join("boot"),
|
||||
config_path: root_path.join("boot"),
|
||||
abs_entries_path: "boot",
|
||||
mount_path: None,
|
||||
},
|
||||
None,
|
||||
),
|
||||
|
||||
Bootloader::Systemd => {
|
||||
let temp_efi_dir = tempfile::tempdir().map_err(|e| {
|
||||
anyhow::anyhow!("Failed to create temporary directory for EFI mount: {e}")
|
||||
})?;
|
||||
|
||||
let mounted_efi = Utf8PathBuf::from_path_buf(temp_efi_dir.path().to_path_buf())
|
||||
.map_err(|_| anyhow::anyhow!("EFI dir is not valid UTF-8"))?;
|
||||
|
||||
Command::new("mount")
|
||||
.args([&PathBuf::from(&esp_device), mounted_efi.as_std_path()])
|
||||
.log_debug()
|
||||
.run_inherited_with_cmd_context()
|
||||
.context("Mounting EFI")?;
|
||||
|
||||
let efi_linux_dir = mounted_efi.join(EFI_LINUX);
|
||||
|
||||
(
|
||||
BLSEntryPath {
|
||||
entries_path: efi_linux_dir,
|
||||
config_path: mounted_efi.clone(),
|
||||
abs_entries_path: EFI_LINUX,
|
||||
mount_path: Some(mounted_efi),
|
||||
},
|
||||
Some(temp_efi_dir),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let (bls_config, boot_digest) = match &entry {
|
||||
ComposefsBootEntry::Type1(..) => unimplemented!(),
|
||||
ComposefsBootEntry::Type2(..) => unimplemented!(),
|
||||
|
||||
ComposefsBootEntry::UsrLibModulesVmLinuz(usr_lib_modules_vmlinuz) => {
|
||||
let boot_digest = compute_boot_digest(usr_lib_modules_vmlinuz, &repo)
|
||||
.context("Computing boot digest")?;
|
||||
|
||||
// Every update should have its own /usr/lib/os-release
|
||||
let (dir, fname) = fs
|
||||
.root
|
||||
.split(OsStr::new("/usr/lib/os-release"))
|
||||
.context("Getting /usr/lib/os-release")?;
|
||||
|
||||
let os_release = dir
|
||||
.get_file_opt(fname)
|
||||
.context("Getting /usr/lib/os-release")?;
|
||||
|
||||
let version = os_release.and_then(|os_rel_file| {
|
||||
let file_contents = match read_file(os_rel_file, &repo) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
tracing::warn!("Could not read /usr/lib/os-release: {e:?}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let file_contents = match std::str::from_utf8(&file_contents) {
|
||||
Ok(c) => c,
|
||||
Err(..) => {
|
||||
tracing::warn!("/usr/lib/os-release did not have valid UTF-8");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
OsReleaseInfo::parse(file_contents).get_version()
|
||||
});
|
||||
|
||||
let default_sort_key = "1";
|
||||
|
||||
let mut bls_config = BLSConfig::default();
|
||||
|
||||
bls_config
|
||||
.with_title(id_hex.clone())
|
||||
.with_sort_key(default_sort_key.into())
|
||||
.with_version(version.unwrap_or(default_sort_key.into()))
|
||||
.with_linux(format!(
|
||||
"/{}/{id_hex}/vmlinuz",
|
||||
entry_paths.abs_entries_path
|
||||
))
|
||||
.with_initrd(vec![format!(
|
||||
"/{}/{id_hex}/initrd",
|
||||
entry_paths.abs_entries_path
|
||||
)])
|
||||
.with_options(cmdline_refs);
|
||||
|
||||
if let Some(symlink_to) = find_vmlinuz_initrd_duplicates(&boot_digest)? {
|
||||
bls_config.linux =
|
||||
format!("/{}/{symlink_to}/vmlinuz", entry_paths.abs_entries_path);
|
||||
|
||||
bls_config.initrd = vec![format!(
|
||||
"/{}/{symlink_to}/initrd",
|
||||
entry_paths.abs_entries_path
|
||||
)];
|
||||
} else {
|
||||
write_bls_boot_entries_to_disk(
|
||||
&entry_paths.entries_path,
|
||||
id,
|
||||
usr_lib_modules_vmlinuz,
|
||||
&repo,
|
||||
)?;
|
||||
}
|
||||
|
||||
(bls_config, boot_digest)
|
||||
}
|
||||
};
|
||||
|
||||
let (config_path, booted_bls) = if is_upgrade {
|
||||
let mut booted_bls = get_booted_bls()?;
|
||||
booted_bls.sort_key = Some("0".into()); // entries are sorted by their filename in reverse order
|
||||
|
||||
// This will be atomically renamed to 'loader/entries' on shutdown/reboot
|
||||
(
|
||||
entry_paths
|
||||
.config_path
|
||||
.join("loader")
|
||||
.join(STAGED_BOOT_LOADER_ENTRIES),
|
||||
Some(booted_bls),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
entry_paths
|
||||
.config_path
|
||||
.join("loader")
|
||||
.join(BOOT_LOADER_ENTRIES),
|
||||
None,
|
||||
)
|
||||
};
|
||||
|
||||
create_dir_all(&config_path).with_context(|| format!("Creating {:?}", config_path))?;
|
||||
|
||||
// Scope to allow for proper unmounting
|
||||
{
|
||||
let loader_entries_dir =
|
||||
cap_std::fs::Dir::open_ambient_dir(&config_path, cap_std::ambient_authority())
|
||||
.with_context(|| format!("Opening {config_path:?}"))?;
|
||||
|
||||
loader_entries_dir.atomic_write(
|
||||
// SAFETY: We set sort_key above
|
||||
format!(
|
||||
"bootc-composefs-{}.conf",
|
||||
bls_config.sort_key.as_ref().unwrap()
|
||||
),
|
||||
bls_config.to_string().as_bytes(),
|
||||
)?;
|
||||
|
||||
if let Some(booted_bls) = booted_bls {
|
||||
loader_entries_dir.atomic_write(
|
||||
// SAFETY: We set sort_key above
|
||||
format!(
|
||||
"bootc-composefs-{}.conf",
|
||||
booted_bls.sort_key.as_ref().unwrap()
|
||||
),
|
||||
booted_bls.to_string().as_bytes(),
|
||||
)?;
|
||||
}
|
||||
|
||||
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")?;
|
||||
}
|
||||
|
||||
if let Some(mounted_efi) = entry_paths.mount_path {
|
||||
Command::new("umount")
|
||||
.arg(mounted_efi)
|
||||
.log_debug()
|
||||
.run_inherited_with_cmd_context()
|
||||
.context("Unmounting EFI")?;
|
||||
}
|
||||
|
||||
Ok(boot_digest)
|
||||
}
|
||||
|
||||
#[context("Setting up UKI boot")]
|
||||
pub(crate) fn setup_composefs_uki_boot(
|
||||
setup_type: BootSetupType,
|
||||
// TODO: Make this generic
|
||||
repo: ComposefsRepository<Sha256HashValue>,
|
||||
id: &Sha256HashValue,
|
||||
entry: ComposefsBootEntry<Sha256HashValue>,
|
||||
) -> Result<()> {
|
||||
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
|
||||
.iter()
|
||||
.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(),
|
||||
state.composefs_options.as_ref().map(|x| x.insecure),
|
||||
)
|
||||
}
|
||||
|
||||
BootSetupType::Upgrade(..) => {
|
||||
let sysroot = Utf8PathBuf::from("/sysroot");
|
||||
let sysroot_parent = get_sysroot_parent_dev()?;
|
||||
|
||||
(sysroot, get_esp_partition(&sysroot_parent)?.0, None)
|
||||
}
|
||||
};
|
||||
|
||||
let temp_efi_dir = tempfile::tempdir()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create temporary directory for EFI mount: {e}"))?;
|
||||
let mounted_efi = temp_efi_dir.path().to_path_buf();
|
||||
|
||||
Task::new("Mounting ESP", "mount")
|
||||
.args([&PathBuf::from(&esp_device), &mounted_efi.clone()])
|
||||
.run()?;
|
||||
|
||||
let boot_label = match entry {
|
||||
ComposefsBootEntry::Type1(..) => unimplemented!(),
|
||||
ComposefsBootEntry::UsrLibModulesVmLinuz(..) => unimplemented!(),
|
||||
|
||||
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_efi.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
|
||||
}
|
||||
};
|
||||
|
||||
Command::new("umount")
|
||||
.arg(&mounted_efi)
|
||||
.log_debug()
|
||||
.run_inherited_with_cmd_context()
|
||||
.context("Unmounting ESP")?;
|
||||
|
||||
let boot_dir = root_path.join("boot");
|
||||
create_dir_all(&boot_dir).context("Failed to create boot dir")?;
|
||||
|
||||
let is_upgrade = matches!(setup_type, BootSetupType::Upgrade(..));
|
||||
|
||||
let efi_uuid_source = get_efi_uuid_source();
|
||||
|
||||
let user_cfg_name = if is_upgrade {
|
||||
USER_CFG_STAGED
|
||||
} else {
|
||||
USER_CFG
|
||||
};
|
||||
|
||||
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 buffer = vec![];
|
||||
|
||||
// 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(),
|
||||
)?;
|
||||
|
||||
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)?;
|
||||
|
||||
// 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())?;
|
||||
|
||||
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(());
|
||||
}
|
||||
|
||||
// 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 esp_uuid = Task::new("blkid for ESP UUID", "blkid")
|
||||
.args(["-s", "UUID", "-o", "value", &esp_device])
|
||||
.read()?;
|
||||
|
||||
grub_dir.atomic_write(
|
||||
EFI_UUID_FILE,
|
||||
format!("set EFI_PART_UUID=\"{}\"", esp_uuid.trim()).as_bytes(),
|
||||
)?;
|
||||
|
||||
// Write to grub2/user.cfg
|
||||
let mut buffer = vec![];
|
||||
|
||||
// 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(())
|
||||
}
|
||||
|
||||
#[context("Setting up composefs boot")]
|
||||
pub(crate) fn setup_composefs_boot(
|
||||
root_setup: &RootSetup,
|
||||
state: &State,
|
||||
image_id: &str,
|
||||
) -> Result<()> {
|
||||
let boot_uuid = root_setup
|
||||
.get_boot_uuid()?
|
||||
.or(root_setup.rootfs_uuid.as_deref())
|
||||
.ok_or_else(|| anyhow!("No uuid for boot/root"))?;
|
||||
|
||||
if cfg!(target_arch = "s390x") {
|
||||
// TODO: Integrate s390x support into install_via_bootupd
|
||||
crate::bootloader::install_via_zipl(&root_setup.device_info, boot_uuid)?;
|
||||
} else {
|
||||
crate::bootloader::install_via_bootupd(
|
||||
&root_setup.device_info,
|
||||
&root_setup.physical_root_path,
|
||||
&state.config_opts,
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
|
||||
let repo = open_composefs_repo(&root_setup.physical_root)?;
|
||||
|
||||
let mut fs = create_composefs_filesystem(&repo, image_id, None)?;
|
||||
|
||||
let entries = fs.transform_for_boot(&repo)?;
|
||||
let id = fs.commit_image(&repo, None)?;
|
||||
|
||||
let Some(entry) = entries.into_iter().next() else {
|
||||
anyhow::bail!("No boot entries!");
|
||||
};
|
||||
|
||||
let boot_type = BootType::from(&entry);
|
||||
let mut boot_digest: Option<String> = None;
|
||||
|
||||
match boot_type {
|
||||
BootType::Bls => {
|
||||
let digest = setup_composefs_bls_boot(
|
||||
BootSetupType::Setup((&root_setup, &state, &fs)),
|
||||
repo,
|
||||
&id,
|
||||
entry,
|
||||
)?;
|
||||
|
||||
boot_digest = Some(digest);
|
||||
}
|
||||
BootType::Uki => setup_composefs_uki_boot(
|
||||
BootSetupType::Setup((&root_setup, &state, &fs)),
|
||||
repo,
|
||||
&id,
|
||||
entry,
|
||||
)?,
|
||||
};
|
||||
|
||||
write_composefs_state(
|
||||
&root_setup.physical_root_path,
|
||||
id,
|
||||
&ImageReference {
|
||||
image: state.source.imageref.name.clone(),
|
||||
transport: state.source.imageref.transport.to_string(),
|
||||
signature: None,
|
||||
},
|
||||
false,
|
||||
boot_type,
|
||||
boot_digest,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
129
crates/lib/src/bootc_composefs/finalize.rs
Normal file
129
crates/lib/src/bootc_composefs/finalize.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
use std::path::Path;
|
||||
|
||||
use crate::bootc_composefs::boot::{get_esp_partition, get_sysroot_parent_dev, BootType};
|
||||
use crate::bootc_composefs::rollback::{rename_exchange_bls_entries, rename_exchange_user_cfg};
|
||||
use crate::spec::Bootloader;
|
||||
use crate::{
|
||||
bootc_composefs::status::composefs_deployment_status, composefs_consts::STATE_DIR_ABS,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use bootc_initramfs_setup::{mount_composefs_image, open_dir};
|
||||
use bootc_mount::tempmount::TempMount;
|
||||
use cap_std_ext::cap_std::{ambient_authority, fs::Dir};
|
||||
use cap_std_ext::dirext::CapStdExtDirExt;
|
||||
use etc_merge::{compute_diff, merge, traverse_etc};
|
||||
use rustix::fs::{fsync, renameat, CWD};
|
||||
use rustix::path::Arg;
|
||||
|
||||
use fn_error_context::context;
|
||||
|
||||
pub(crate) async fn composefs_native_finalize() -> Result<()> {
|
||||
let host = composefs_deployment_status().await?;
|
||||
|
||||
let booted_composefs = host.require_composefs_booted()?;
|
||||
|
||||
let Some(staged_depl) = host.status.staged.as_ref() else {
|
||||
tracing::debug!("No staged deployment found");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let staged_composefs = staged_depl.composefs.as_ref().ok_or(anyhow::anyhow!(
|
||||
"Staged deployment is not a composefs deployment"
|
||||
))?;
|
||||
|
||||
// Mount the booted EROFS image to get pristine etc
|
||||
let sysroot = open_dir(CWD, "/sysroot")?;
|
||||
let composefs_fd = mount_composefs_image(&sysroot, &booted_composefs.verity, false)?;
|
||||
|
||||
let erofs_tmp_mnt = TempMount::mount_fd(&composefs_fd)?;
|
||||
|
||||
// Perform the /etc merge
|
||||
let pristine_etc =
|
||||
Dir::open_ambient_dir(erofs_tmp_mnt.dir.path().join("etc"), ambient_authority())?;
|
||||
let current_etc = Dir::open_ambient_dir("/etc", ambient_authority())?;
|
||||
|
||||
let new_etc_path = Path::new(STATE_DIR_ABS)
|
||||
.join(&staged_composefs.verity)
|
||||
.join("etc");
|
||||
|
||||
let new_etc = Dir::open_ambient_dir(new_etc_path, ambient_authority())?;
|
||||
|
||||
let (pristine_files, current_files, new_files) =
|
||||
traverse_etc(&pristine_etc, ¤t_etc, &new_etc)?;
|
||||
|
||||
let diff = compute_diff(&pristine_files, ¤t_files)?;
|
||||
merge(¤t_etc, ¤t_files, &new_etc, &new_files, diff)?;
|
||||
|
||||
// Unmount EROFS
|
||||
drop(erofs_tmp_mnt);
|
||||
|
||||
let sysroot_parent = get_sysroot_parent_dev()?;
|
||||
// NOTE: Assumption here that ESP will always be present
|
||||
let (esp_part, ..) = get_esp_partition(&sysroot_parent)?;
|
||||
|
||||
let esp_mount = TempMount::mount_dev(&esp_part)?;
|
||||
let boot_dir = Dir::open_ambient_dir("/sysroot/boot", ambient_authority())
|
||||
.context("Opening sysroot/boot")?;
|
||||
|
||||
// NOTE: Assuming here we won't have two bootloaders at the same time
|
||||
match booted_composefs.bootloader {
|
||||
Bootloader::Grub => match staged_composefs.boot_type {
|
||||
BootType::Bls => {
|
||||
let entries_dir = boot_dir.open_dir("loader")?;
|
||||
rename_exchange_bls_entries(&entries_dir)?;
|
||||
}
|
||||
BootType::Uki => finalize_staged_grub_uki(&esp_mount.fd, &boot_dir)?,
|
||||
},
|
||||
|
||||
Bootloader::Systemd => match staged_composefs.boot_type {
|
||||
BootType::Bls => {
|
||||
let entries_dir = esp_mount.fd.open_dir("loader")?;
|
||||
rename_exchange_bls_entries(&entries_dir)?;
|
||||
}
|
||||
BootType::Uki => rename_staged_uki_entries(&esp_mount.fd)?,
|
||||
},
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[context("Grub: Finalizing staged UKI")]
|
||||
fn finalize_staged_grub_uki(esp_mount: &Dir, boot_fd: &Dir) -> Result<()> {
|
||||
rename_staged_uki_entries(esp_mount)?;
|
||||
|
||||
let entries_dir = boot_fd.open_dir("grub2")?;
|
||||
rename_exchange_user_cfg(&entries_dir)?;
|
||||
|
||||
let entries_dir = entries_dir.reopen_as_ownedfd()?;
|
||||
fsync(entries_dir).context("fsync")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[context("Renaming staged UKI entries")]
|
||||
fn rename_staged_uki_entries(esp_mount: &Dir) -> Result<()> {
|
||||
for entry in esp_mount.entries()? {
|
||||
let entry = entry?;
|
||||
|
||||
let filename = entry.file_name();
|
||||
let filename = filename.as_str()?;
|
||||
|
||||
if !filename.ends_with(".staged") {
|
||||
continue;
|
||||
}
|
||||
|
||||
renameat(
|
||||
&esp_mount,
|
||||
filename,
|
||||
&esp_mount,
|
||||
// SAFETY: We won't reach here if not for the above condition
|
||||
filename.strip_suffix(".staged").unwrap(),
|
||||
)
|
||||
.context("Renaming {filename}")?;
|
||||
}
|
||||
|
||||
let esp_mount = esp_mount.reopen_as_ownedfd()?;
|
||||
fsync(esp_mount).context("fsync")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
8
crates/lib/src/bootc_composefs/mod.rs
Normal file
8
crates/lib/src/bootc_composefs/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
pub(crate) mod boot;
|
||||
pub(crate) mod finalize;
|
||||
pub(crate) mod repo;
|
||||
pub(crate) mod rollback;
|
||||
pub(crate) mod state;
|
||||
pub(crate) mod status;
|
||||
pub(crate) mod switch;
|
||||
pub(crate) mod update;
|
||||
88
crates/lib/src/bootc_composefs/repo.rs
Normal file
88
crates/lib/src/bootc_composefs/repo.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use fn_error_context::context;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use ostree_ext::composefs::{
|
||||
fsverity::{FsVerityHashValue, Sha256HashValue},
|
||||
repository::Repository as ComposefsRepository,
|
||||
tree::FileSystem,
|
||||
util::Sha256Digest,
|
||||
};
|
||||
use ostree_ext::composefs_boot::{bootloader::BootEntry as ComposefsBootEntry, BootOps};
|
||||
use ostree_ext::composefs_oci::{
|
||||
image::create_filesystem as create_composefs_filesystem, pull as composefs_oci_pull,
|
||||
};
|
||||
|
||||
use ostree_ext::container::ImageReference as OstreeExtImgRef;
|
||||
|
||||
use cap_std_ext::cap_std::{ambient_authority, fs::Dir};
|
||||
|
||||
use crate::install::{RootSetup, State};
|
||||
|
||||
pub(crate) fn open_composefs_repo(
|
||||
rootfs_dir: &Dir,
|
||||
) -> Result<ComposefsRepository<Sha256HashValue>> {
|
||||
ComposefsRepository::open_path(rootfs_dir, "composefs")
|
||||
.context("Failed to open composefs repository")
|
||||
}
|
||||
|
||||
pub(crate) async fn initialize_composefs_repository(
|
||||
state: &State,
|
||||
root_setup: &RootSetup,
|
||||
) -> Result<(Sha256Digest, impl FsVerityHashValue)> {
|
||||
let rootfs_dir = &root_setup.physical_root;
|
||||
|
||||
rootfs_dir
|
||||
.create_dir_all("composefs")
|
||||
.context("Creating dir composefs")?;
|
||||
|
||||
let repo = open_composefs_repo(rootfs_dir)?;
|
||||
|
||||
let OstreeExtImgRef {
|
||||
name: image_name,
|
||||
transport,
|
||||
} = &state.source.imageref;
|
||||
|
||||
// transport's display is already of type "<transport_type>:"
|
||||
composefs_oci_pull(
|
||||
&Arc::new(repo),
|
||||
&format!("{transport}{image_name}"),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Pulls the `image` from `transport` into a composefs repository at /sysroot
|
||||
/// Checks for boot entries in the image and returns them
|
||||
#[context("Pulling composefs repository")]
|
||||
pub(crate) async fn pull_composefs_repo(
|
||||
transport: &String,
|
||||
image: &String,
|
||||
) -> Result<(
|
||||
ComposefsRepository<Sha256HashValue>,
|
||||
Vec<ComposefsBootEntry<Sha256HashValue>>,
|
||||
Sha256HashValue,
|
||||
FileSystem<Sha256HashValue>,
|
||||
)> {
|
||||
let rootfs_dir = Dir::open_ambient_dir("/sysroot", ambient_authority())?;
|
||||
|
||||
let repo = open_composefs_repo(&rootfs_dir).context("Opening compoesfs repo")?;
|
||||
|
||||
let (id, verity) =
|
||||
composefs_oci_pull(&Arc::new(repo), &format!("{transport}:{image}"), None, None)
|
||||
.await
|
||||
.context("Pulling composefs repo")?;
|
||||
|
||||
tracing::info!("id: {}, verity: {}", hex::encode(id), verity.to_hex());
|
||||
|
||||
let repo = open_composefs_repo(&rootfs_dir)?;
|
||||
let mut fs = create_composefs_filesystem(&repo, &hex::encode(id), None)
|
||||
.context("Failed to create composefs filesystem")?;
|
||||
|
||||
let entries = fs.transform_for_boot(&repo)?;
|
||||
let id = fs.commit_image(&repo, None)?;
|
||||
|
||||
Ok((repo, entries, id, fs))
|
||||
}
|
||||
198
crates/lib/src/bootc_composefs/rollback.rs
Normal file
198
crates/lib/src/bootc_composefs/rollback.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{fmt::Write, fs::create_dir_all};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use cap_std_ext::cap_std::fs::Dir;
|
||||
use cap_std_ext::{cap_std, dirext::CapStdExtDirExt};
|
||||
use fn_error_context::context;
|
||||
use rustix::fs::{fsync, renameat_with, AtFlags, RenameFlags};
|
||||
|
||||
use crate::bootc_composefs::boot::BootType;
|
||||
use crate::bootc_composefs::status::{composefs_deployment_status, get_sorted_bls_boot_entries};
|
||||
use crate::{
|
||||
bootc_composefs::{boot::get_efi_uuid_source, status::get_sorted_uki_boot_entries},
|
||||
composefs_consts::{
|
||||
BOOT_LOADER_ENTRIES, STAGED_BOOT_LOADER_ENTRIES, USER_CFG, USER_CFG_STAGED,
|
||||
},
|
||||
spec::BootOrder,
|
||||
};
|
||||
|
||||
pub(crate) fn rename_exchange_user_cfg(entries_dir: &Dir) -> Result<()> {
|
||||
tracing::debug!("Atomically exchanging {USER_CFG_STAGED} and {USER_CFG}");
|
||||
renameat_with(
|
||||
&entries_dir,
|
||||
USER_CFG_STAGED,
|
||||
&entries_dir,
|
||||
USER_CFG,
|
||||
RenameFlags::EXCHANGE,
|
||||
)
|
||||
.context("renameat")?;
|
||||
|
||||
tracing::debug!("Removing {USER_CFG_STAGED}");
|
||||
rustix::fs::unlinkat(&entries_dir, USER_CFG_STAGED, AtFlags::empty()).context("unlinkat")?;
|
||||
|
||||
tracing::debug!("Syncing to disk");
|
||||
let entries_dir = entries_dir
|
||||
.reopen_as_ownedfd()
|
||||
.context(format!("Reopening entries dir as owned fd"))?;
|
||||
|
||||
fsync(entries_dir).context(format!("fsync entries dir"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn rename_exchange_bls_entries(entries_dir: &Dir) -> Result<()> {
|
||||
tracing::debug!("Atomically exchanging {STAGED_BOOT_LOADER_ENTRIES} and {BOOT_LOADER_ENTRIES}");
|
||||
renameat_with(
|
||||
&entries_dir,
|
||||
STAGED_BOOT_LOADER_ENTRIES,
|
||||
&entries_dir,
|
||||
BOOT_LOADER_ENTRIES,
|
||||
RenameFlags::EXCHANGE,
|
||||
)
|
||||
.context("renameat")?;
|
||||
|
||||
tracing::debug!("Removing {STAGED_BOOT_LOADER_ENTRIES}");
|
||||
rustix::fs::unlinkat(&entries_dir, STAGED_BOOT_LOADER_ENTRIES, AtFlags::REMOVEDIR)
|
||||
.context("unlinkat")?;
|
||||
|
||||
tracing::debug!("Syncing to disk");
|
||||
let entries_dir = entries_dir
|
||||
.reopen_as_ownedfd()
|
||||
.with_context(|| format!("Reopening /sysroot/boot/loader as owned fd"))?;
|
||||
|
||||
fsync(entries_dir).context("fsync")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[context("Rolling back UKI")]
|
||||
pub(crate) fn rollback_composefs_uki() -> Result<()> {
|
||||
let user_cfg_path = PathBuf::from("/sysroot/boot/grub2");
|
||||
|
||||
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(Johan-Liebert): Currently assuming there are only two deployments
|
||||
assert!(menuentries.len() == 2);
|
||||
|
||||
let (first, second) = menuentries.split_at_mut(1);
|
||||
std::mem::swap(&mut first[0], &mut second[0]);
|
||||
|
||||
let mut buffer = get_efi_uuid_source();
|
||||
|
||||
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_STAGED, buffer)
|
||||
.with_context(|| format!("Writing to {USER_CFG_STAGED}"))?;
|
||||
|
||||
rename_exchange_user_cfg(&entries_dir)
|
||||
}
|
||||
|
||||
#[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_bls_boot_entries(&boot_dir, false)?;
|
||||
|
||||
// Update the indicies so that they're swapped
|
||||
for (idx, cfg) in all_configs.iter_mut().enumerate() {
|
||||
cfg.sort_key = Some(idx.to_string());
|
||||
}
|
||||
|
||||
// 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/{STAGED_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 {
|
||||
// SAFETY: We set sort_key above
|
||||
let file_name = format!("bootc-composefs-{}.conf", cfg.sort_key.as_ref().unwrap());
|
||||
|
||||
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 = Dir::open_ambient_dir("/sysroot/boot/loader", cap_std::ambient_authority())
|
||||
.context("Opening loader dir")?;
|
||||
|
||||
rename_exchange_bls_entries(&dir)
|
||||
}
|
||||
|
||||
#[context("Rolling back composefs")]
|
||||
pub(crate) async fn composefs_rollback() -> Result<()> {
|
||||
let host = composefs_deployment_status().await?;
|
||||
|
||||
let new_spec = {
|
||||
let mut new_spec = host.spec.clone();
|
||||
new_spec.boot_order = new_spec.boot_order.swap();
|
||||
new_spec
|
||||
};
|
||||
|
||||
// Just to be sure
|
||||
host.spec.verify_transition(&new_spec)?;
|
||||
|
||||
let reverting = new_spec.boot_order == BootOrder::Default;
|
||||
if reverting {
|
||||
println!("notice: Reverting queued rollback state");
|
||||
}
|
||||
|
||||
let rollback_status = host
|
||||
.status
|
||||
.rollback
|
||||
.ok_or_else(|| anyhow!("No rollback available"))?;
|
||||
|
||||
// TODO: Handle staged deployment
|
||||
// Ostree will drop any staged deployment on rollback but will keep it if it is the first item
|
||||
// in the new deployment list
|
||||
let Some(rollback_composefs_entry) = &rollback_status.composefs else {
|
||||
anyhow::bail!("Rollback deployment not a composefs deployment")
|
||||
};
|
||||
|
||||
match rollback_composefs_entry.boot_type {
|
||||
BootType::Bls => rollback_composefs_bls(),
|
||||
BootType::Uki => rollback_composefs_uki(),
|
||||
}?;
|
||||
|
||||
if reverting {
|
||||
println!("Next boot: current deployment");
|
||||
} else {
|
||||
println!("Next boot: rollback deployment");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
165
crates/lib/src/bootc_composefs/state.rs
Normal file
165
crates/lib/src/bootc_composefs/state.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
use std::os::unix::fs::symlink;
|
||||
use std::{fs::create_dir_all, process::Command};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use bootc_kernel_cmdline::utf8::Cmdline;
|
||||
use bootc_mount::tempmount::TempMount;
|
||||
use bootc_utils::CommandRunExt;
|
||||
use camino::Utf8PathBuf;
|
||||
use cap_std_ext::{cap_std, dirext::CapStdExtDirExt};
|
||||
use composefs::fsverity::{FsVerityHashValue, Sha256HashValue};
|
||||
use fn_error_context::context;
|
||||
|
||||
use ostree_ext::container::deploy::ORIGIN_CONTAINER;
|
||||
use rustix::{
|
||||
fs::{open, Mode, OFlags},
|
||||
path::Arg,
|
||||
};
|
||||
|
||||
use crate::bootc_composefs::boot::BootType;
|
||||
use crate::{
|
||||
composefs_consts::{
|
||||
COMPOSEFS_CMDLINE, COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR,
|
||||
ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_DIGEST, ORIGIN_KEY_BOOT_TYPE, SHARED_VAR_PATH,
|
||||
STATE_DIR_RELATIVE,
|
||||
},
|
||||
parsers::bls_config::{parse_bls_config, BLSConfig},
|
||||
spec::ImageReference,
|
||||
utils::path_relative_to,
|
||||
};
|
||||
|
||||
pub(crate) fn get_booted_bls() -> Result<BLSConfig> {
|
||||
let cmdline = Cmdline::from_proc()?;
|
||||
let booted = cmdline
|
||||
.find(COMPOSEFS_CMDLINE)
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to find composefs parameter in kernel cmdline"))?;
|
||||
|
||||
for entry in std::fs::read_dir("/sysroot/boot/loader/entries")? {
|
||||
let entry = entry?;
|
||||
|
||||
if !entry.file_name().as_str()?.ends_with(".conf") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let bls = parse_bls_config(&std::fs::read_to_string(&entry.path())?)?;
|
||||
|
||||
let Some(opts) = &bls.options else {
|
||||
anyhow::bail!("options not found in bls config")
|
||||
};
|
||||
let opts = Cmdline::from(opts);
|
||||
|
||||
if opts.iter().any(|v| v == booted) {
|
||||
return Ok(bls);
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!("Booted BLS not found"))
|
||||
}
|
||||
|
||||
/// Mounts an EROFS image and copies the pristine /etc to the deployment's /etc
|
||||
#[context("Copying etc")]
|
||||
pub(crate) fn copy_etc_to_state(
|
||||
sysroot_path: &Utf8PathBuf,
|
||||
erofs_id: &String,
|
||||
state_path: &Utf8PathBuf,
|
||||
) -> Result<()> {
|
||||
let sysroot_fd = open(
|
||||
sysroot_path.as_std_path(),
|
||||
OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
|
||||
Mode::empty(),
|
||||
)
|
||||
.context("Opening sysroot")?;
|
||||
|
||||
let composefs_fd = bootc_initramfs_setup::mount_composefs_image(&sysroot_fd, &erofs_id, false)?;
|
||||
|
||||
let tempdir = TempMount::mount_fd(composefs_fd)?;
|
||||
|
||||
// TODO: Replace this with a function to cap_std_ext
|
||||
let cp_ret = Command::new("cp")
|
||||
.args([
|
||||
"-a",
|
||||
&format!("{}/etc/.", tempdir.dir.path().as_str()?),
|
||||
&format!("{state_path}/etc/."),
|
||||
])
|
||||
.run_capture_stderr();
|
||||
|
||||
cp_ret
|
||||
}
|
||||
|
||||
/// Creates and populates /sysroot/state/deploy/image_id
|
||||
#[context("Writing composefs state")]
|
||||
pub(crate) fn write_composefs_state(
|
||||
root_path: &Utf8PathBuf,
|
||||
deployment_id: Sha256HashValue,
|
||||
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"))?;
|
||||
|
||||
copy_etc_to_state(&root_path, &deployment_id.to_hex(), &state_path)?;
|
||||
|
||||
let actual_var_path = root_path.join(SHARED_VAR_PATH);
|
||||
create_dir_all(&actual_var_path)?;
|
||||
|
||||
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,
|
||||
transport,
|
||||
..
|
||||
} = &imgref;
|
||||
|
||||
let mut config = tini::Ini::new().section("origin").item(
|
||||
ORIGIN_CONTAINER,
|
||||
format!("ostree-unverified-image:{transport}{image_name}"),
|
||||
);
|
||||
|
||||
config = config
|
||||
.section(ORIGIN_KEY_BOOT)
|
||||
.item(ORIGIN_KEY_BOOT_TYPE, boot_type);
|
||||
|
||||
if let Some(boot_digest) = boot_digest {
|
||||
config = config
|
||||
.section(ORIGIN_KEY_BOOT)
|
||||
.item(ORIGIN_KEY_BOOT_DIGEST, boot_digest);
|
||||
}
|
||||
|
||||
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("Failed 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 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}"))?;
|
||||
|
||||
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(())
|
||||
}
|
||||
505
crates/lib/src/bootc_composefs/status.rs
Normal file
505
crates/lib/src/bootc_composefs/status.rs
Normal file
@@ -0,0 +1,505 @@
|
||||
use std::{io::Read, sync::OnceLock};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use bootc_kernel_cmdline::utf8::Cmdline;
|
||||
use fn_error_context::context;
|
||||
|
||||
use crate::{
|
||||
bootc_composefs::boot::BootType,
|
||||
composefs_consts::{BOOT_LOADER_ENTRIES, COMPOSEFS_CMDLINE, USER_CFG},
|
||||
parsers::{
|
||||
bls_config::{parse_bls_config, BLSConfig},
|
||||
grub_menuconfig::{parse_grub_menuentry_file, MenuEntry},
|
||||
},
|
||||
spec::{BootEntry, BootOrder, Host, HostSpec, ImageReference, ImageStatus},
|
||||
};
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use bootc_utils::try_deserialize_timestamp;
|
||||
use cap_std_ext::cap_std::ambient_authority;
|
||||
use cap_std_ext::cap_std::fs::Dir;
|
||||
use ostree_container::OstreeImageReference;
|
||||
use ostree_ext::container::deploy::ORIGIN_CONTAINER;
|
||||
use ostree_ext::container::{self as ostree_container};
|
||||
use ostree_ext::containers_image_proxy;
|
||||
use ostree_ext::oci_spec;
|
||||
|
||||
use ostree_ext::oci_spec::image::ImageManifest;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use crate::composefs_consts::{
|
||||
COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR, ORIGIN_KEY_BOOT,
|
||||
ORIGIN_KEY_BOOT_TYPE, STATE_DIR_RELATIVE,
|
||||
};
|
||||
use crate::install::EFIVARFS;
|
||||
use crate::spec::Bootloader;
|
||||
|
||||
/// A parsed composefs command line
|
||||
pub(crate) struct ComposefsCmdline {
|
||||
#[allow(dead_code)]
|
||||
pub insecure: bool,
|
||||
pub digest: Box<str>,
|
||||
}
|
||||
|
||||
impl ComposefsCmdline {
|
||||
pub(crate) fn new(s: &str) -> Self {
|
||||
let (insecure, digest_str) = s
|
||||
.strip_prefix('?')
|
||||
.map(|v| (true, v))
|
||||
.unwrap_or_else(|| (false, s));
|
||||
ComposefsCmdline {
|
||||
insecure,
|
||||
digest: digest_str.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ComposefsCmdline {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let insecure = if self.insecure { "?" } else { "" };
|
||||
write!(f, "{}={}{}", COMPOSEFS_CMDLINE, insecure, self.digest)
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect if we have composefs=<digest> in /proc/cmdline
|
||||
pub(crate) fn composefs_booted() -> Result<Option<&'static ComposefsCmdline>> {
|
||||
static CACHED_DIGEST_VALUE: OnceLock<Option<ComposefsCmdline>> = OnceLock::new();
|
||||
if let Some(v) = CACHED_DIGEST_VALUE.get() {
|
||||
return Ok(v.as_ref());
|
||||
}
|
||||
let cmdline = Cmdline::from_proc()?;
|
||||
let Some(kv) = cmdline.find(COMPOSEFS_CMDLINE) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(v) = kv.value() else { return Ok(None) };
|
||||
let v = ComposefsCmdline::new(v);
|
||||
let r = CACHED_DIGEST_VALUE.get_or_init(|| Some(v));
|
||||
Ok(r.as_ref())
|
||||
}
|
||||
|
||||
// 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 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 boot_dir.read_dir(format!("loader/{BOOT_LOADER_ENTRIES}"))? {
|
||||
let entry = entry?;
|
||||
|
||||
let file_name = entry.file_name();
|
||||
|
||||
let file_name = file_name
|
||||
.to_str()
|
||||
.ok_or(anyhow::anyhow!("Found non UTF-8 characters in filename"))?;
|
||||
|
||||
if !file_name.ends_with(".conf") {
|
||||
continue;
|
||||
}
|
||||
|
||||
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")?;
|
||||
|
||||
all_configs.push(config);
|
||||
}
|
||||
|
||||
all_configs.sort_by(|a, b| if ascending { a.cmp(b) } else { b.cmp(a) });
|
||||
|
||||
return Ok(all_configs);
|
||||
}
|
||||
|
||||
/// imgref = transport:image_name
|
||||
#[context("Getting container info")]
|
||||
async fn get_container_manifest_and_config(
|
||||
imgref: &String,
|
||||
) -> Result<(ImageManifest, oci_spec::image::ImageConfiguration)> {
|
||||
let config = containers_image_proxy::ImageProxyConfig::default();
|
||||
let proxy = containers_image_proxy::ImageProxy::new_with_config(config).await?;
|
||||
|
||||
let img = proxy.open_image(&imgref).await.context("Opening image")?;
|
||||
|
||||
let (_, manifest) = proxy.fetch_manifest(&img).await?;
|
||||
let (mut reader, driver) = proxy.get_descriptor(&img, manifest.config()).await?;
|
||||
|
||||
let mut buf = Vec::with_capacity(manifest.config().size() as usize);
|
||||
buf.resize(manifest.config().size() as usize, 0);
|
||||
reader.read_exact(&mut buf).await?;
|
||||
driver.await?;
|
||||
|
||||
let config: oci_spec::image::ImageConfiguration = serde_json::from_slice(&buf)?;
|
||||
|
||||
Ok((manifest, config))
|
||||
}
|
||||
|
||||
#[context("Getting bootloader")]
|
||||
fn get_bootloader() -> Result<Bootloader> {
|
||||
let efivarfs = match Dir::open_ambient_dir(EFIVARFS, ambient_authority()) {
|
||||
Ok(dir) => dir,
|
||||
// Most likely using BIOS
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Bootloader::Grub),
|
||||
Err(e) => Err(e).context(format!("Opening {EFIVARFS}"))?,
|
||||
};
|
||||
|
||||
const EFI_LOADER_INFO: &str = "LoaderInfo-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f";
|
||||
|
||||
match efivarfs.read_to_string(EFI_LOADER_INFO) {
|
||||
Ok(loader) => {
|
||||
if loader.to_lowercase().contains("systemd-boot") {
|
||||
return Ok(Bootloader::Systemd);
|
||||
}
|
||||
|
||||
return Ok(Bootloader::Grub);
|
||||
}
|
||||
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Bootloader::Grub),
|
||||
|
||||
Err(e) => Err(e).context(format!("Opening {EFI_LOADER_INFO}"))?,
|
||||
}
|
||||
}
|
||||
|
||||
#[context("Getting composefs deployment metadata")]
|
||||
async fn boot_entry_from_composefs_deployment(
|
||||
origin: tini::Ini,
|
||||
verity: String,
|
||||
) -> Result<BootEntry> {
|
||||
let image = match origin.get::<String>("origin", ORIGIN_CONTAINER) {
|
||||
Some(img_name_from_config) => {
|
||||
let ostree_img_ref = OstreeImageReference::from_str(&img_name_from_config)?;
|
||||
let imgref = ostree_img_ref.imgref.to_string();
|
||||
let img_ref = ImageReference::from(ostree_img_ref);
|
||||
|
||||
// The image might've been removed, so don't error if we can't get the image manifest
|
||||
let (image_digest, version, architecture, created_at) =
|
||||
match get_container_manifest_and_config(&imgref).await {
|
||||
Ok((manifest, config)) => {
|
||||
let digest = manifest.config().digest().to_string();
|
||||
let arch = config.architecture().to_string();
|
||||
let created = config.created().clone();
|
||||
let version = manifest
|
||||
.annotations()
|
||||
.as_ref()
|
||||
.and_then(|a| a.get(oci_spec::image::ANNOTATION_VERSION).cloned());
|
||||
|
||||
(digest, version, arch, created)
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
tracing::debug!("Failed to open image {img_ref}, because {e:?}");
|
||||
("".into(), None, "".into(), None)
|
||||
}
|
||||
};
|
||||
|
||||
let timestamp = created_at.and_then(|x| try_deserialize_timestamp(&x));
|
||||
|
||||
let image_status = ImageStatus {
|
||||
image: img_ref,
|
||||
version,
|
||||
timestamp,
|
||||
image_digest,
|
||||
architecture,
|
||||
};
|
||||
|
||||
Some(image_status)
|
||||
}
|
||||
|
||||
// Wasn't booted using a container image. Do nothing
|
||||
None => None,
|
||||
};
|
||||
|
||||
let boot_type = match origin.get::<String>(ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_TYPE) {
|
||||
Some(s) => BootType::try_from(s.as_str())?,
|
||||
None => anyhow::bail!("{ORIGIN_KEY_BOOT} not found"),
|
||||
};
|
||||
|
||||
let e = BootEntry {
|
||||
image,
|
||||
cached_update: None,
|
||||
incompatible: false,
|
||||
pinned: false,
|
||||
store: None,
|
||||
ostree: None,
|
||||
composefs: Some(crate::spec::BootEntryComposefs {
|
||||
verity,
|
||||
boot_type,
|
||||
bootloader: get_bootloader()?,
|
||||
}),
|
||||
soft_reboot_capable: false,
|
||||
};
|
||||
|
||||
return Ok(e);
|
||||
}
|
||||
|
||||
#[context("Getting composefs deployment status")]
|
||||
pub(crate) async fn composefs_deployment_status() -> Result<Host> {
|
||||
let composefs_state = composefs_booted()?
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to find composefs parameter in kernel cmdline"))?;
|
||||
let composefs_digest = &composefs_state.digest;
|
||||
|
||||
let sysroot =
|
||||
Dir::open_ambient_dir("/sysroot", ambient_authority()).context("Opening sysroot")?;
|
||||
let deployments = sysroot
|
||||
.read_dir(STATE_DIR_RELATIVE)
|
||||
.with_context(|| format!("Reading sysroot {STATE_DIR_RELATIVE}"))?;
|
||||
|
||||
let host_spec = HostSpec {
|
||||
image: None,
|
||||
boot_order: BootOrder::Default,
|
||||
};
|
||||
|
||||
let mut host = Host::new(host_spec);
|
||||
|
||||
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)?;
|
||||
|
||||
Ok(Some(s))
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||
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?;
|
||||
|
||||
let depl_file_name = depl.file_name();
|
||||
let depl_file_name = depl_file_name.to_string_lossy();
|
||||
|
||||
// read the origin file
|
||||
let config = depl
|
||||
.open_dir()
|
||||
.with_context(|| format!("Failed to open {depl_file_name}"))?
|
||||
.read_to_string(format!("{depl_file_name}.origin"))
|
||||
.with_context(|| format!("Reading file {depl_file_name}.origin"))?;
|
||||
|
||||
let ini = tini::Ini::from_string(&config)
|
||||
.with_context(|| format!("Failed to parse file {depl_file_name}.origin as ini"))?;
|
||||
|
||||
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() == composefs_digest.as_ref() {
|
||||
host.spec.image = boot_entry.image.as_ref().map(|x| x.image.clone());
|
||||
host.status.booted = Some(boot_entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(staged_deployment_id) = &staged_deployment_id {
|
||||
if depl_file_name == staged_deployment_id.trim() {
|
||||
host.status.staged = Some(boot_entry);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
host.status.rollback = Some(boot_entry);
|
||||
}
|
||||
|
||||
// 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
|
||||
.as_ref()
|
||||
.ok_or(anyhow::anyhow!("options key not found in bls config"))?
|
||||
.contains(composefs_digest.as_ref());
|
||||
}
|
||||
|
||||
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(composefs_digest.as_ref())
|
||||
}
|
||||
};
|
||||
|
||||
if host.status.rollback_queued {
|
||||
host.spec.boot_order = BootOrder::Rollback
|
||||
};
|
||||
|
||||
Ok(host)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use cap_std_ext::{cap_std, dirext::CapStdExtDirExt};
|
||||
|
||||
use crate::parsers::grub_menuconfig::MenuentryBody;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_composefs_parsing() {
|
||||
const DIGEST: &str = "8b7df143d91c716ecfa5fc1730022f6b421b05cedee8fd52b1fc65a96030ad52";
|
||||
let v = ComposefsCmdline::new(DIGEST);
|
||||
assert!(!v.insecure);
|
||||
assert_eq!(v.digest.as_ref(), DIGEST);
|
||||
let v = ComposefsCmdline::new(&format!("?{}", DIGEST));
|
||||
assert!(v.insecure);
|
||||
assert_eq!(v.digest.as_ref(), DIGEST);
|
||||
}
|
||||
|
||||
#[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 fedora-42.0
|
||||
sort-key 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 fedora-42.0
|
||||
sort-key 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).unwrap();
|
||||
|
||||
let mut config1 = BLSConfig::default();
|
||||
config1.title = Some("Fedora 42.20250623.3.1 (CoreOS)".into());
|
||||
config1.sort_key = Some("1".into());
|
||||
config1.linux = "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10".into();
|
||||
config1.initrd = vec!["/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img".into()];
|
||||
config1.options = Some("root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6".into());
|
||||
|
||||
let mut config2 = BLSConfig::default();
|
||||
config2.title = Some("Fedora 41.20250214.2.0 (CoreOS)".into());
|
||||
config2.sort_key = Some("2".into());
|
||||
config2.linux = "/boot/febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01/vmlinuz-5.14.10".into();
|
||||
config2.initrd = vec!["/boot/febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01/initramfs-5.14.10.img".into()];
|
||||
config2.options = Some("root=UUID=abc123 rw composefs=febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01".into());
|
||||
|
||||
assert_eq!(result[0].sort_key.as_ref().unwrap(), "1");
|
||||
assert_eq!(result[1].sort_key.as_ref().unwrap(), "2");
|
||||
|
||||
let result = get_sorted_bls_boot_entries(&tempdir, false).unwrap();
|
||||
assert_eq!(result[0].sort_key.as_ref().unwrap(), "2");
|
||||
assert_eq!(result[1].sort_key.as_ref().unwrap(), "1");
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
73
crates/lib/src/bootc_composefs/switch.rs
Normal file
73
crates/lib/src/bootc_composefs/switch.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use anyhow::{Context, Result};
|
||||
use camino::Utf8PathBuf;
|
||||
use fn_error_context::context;
|
||||
|
||||
use crate::{
|
||||
bootc_composefs::{
|
||||
boot::{setup_composefs_bls_boot, setup_composefs_uki_boot, BootSetupType, BootType},
|
||||
repo::pull_composefs_repo,
|
||||
state::write_composefs_state,
|
||||
status::composefs_deployment_status,
|
||||
},
|
||||
cli::{imgref_for_switch, SwitchOpts},
|
||||
};
|
||||
|
||||
#[context("Composefs Switching")]
|
||||
pub(crate) async fn switch_composefs(opts: SwitchOpts) -> Result<()> {
|
||||
let target = imgref_for_switch(&opts)?;
|
||||
// TODO: Handle in-place
|
||||
|
||||
let host = composefs_deployment_status()
|
||||
.await
|
||||
.context("Getting composefs deployment status")?;
|
||||
|
||||
let new_spec = {
|
||||
let mut new_spec = host.spec.clone();
|
||||
new_spec.image = Some(target.clone());
|
||||
new_spec
|
||||
};
|
||||
|
||||
if new_spec == host.spec {
|
||||
println!("Image specification is unchanged.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(target_imgref) = new_spec.image else {
|
||||
anyhow::bail!("Target image is undefined")
|
||||
};
|
||||
|
||||
let (repo, entries, id, fs) =
|
||||
pull_composefs_repo(&target_imgref.transport, &target_imgref.image).await?;
|
||||
|
||||
let Some(entry) = entries.into_iter().next() else {
|
||||
anyhow::bail!("No boot entries!");
|
||||
};
|
||||
|
||||
let boot_type = BootType::from(&entry);
|
||||
let mut boot_digest = None;
|
||||
|
||||
match boot_type {
|
||||
BootType::Bls => {
|
||||
boot_digest = Some(setup_composefs_bls_boot(
|
||||
BootSetupType::Upgrade((&fs, &host)),
|
||||
repo,
|
||||
&id,
|
||||
entry,
|
||||
)?)
|
||||
}
|
||||
BootType::Uki => {
|
||||
setup_composefs_uki_boot(BootSetupType::Upgrade((&fs, &host)), repo, &id, entry)?
|
||||
}
|
||||
};
|
||||
|
||||
write_composefs_state(
|
||||
&Utf8PathBuf::from("/sysroot"),
|
||||
id,
|
||||
&target_imgref,
|
||||
true,
|
||||
boot_type,
|
||||
boot_digest,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
64
crates/lib/src/bootc_composefs/update.rs
Normal file
64
crates/lib/src/bootc_composefs/update.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
use anyhow::{Context, Result};
|
||||
use camino::Utf8PathBuf;
|
||||
use fn_error_context::context;
|
||||
|
||||
use crate::{
|
||||
bootc_composefs::{
|
||||
boot::{setup_composefs_bls_boot, setup_composefs_uki_boot, BootSetupType, BootType},
|
||||
repo::pull_composefs_repo,
|
||||
state::write_composefs_state,
|
||||
status::composefs_deployment_status,
|
||||
},
|
||||
cli::UpgradeOpts,
|
||||
};
|
||||
|
||||
#[context("Upgrading composefs")]
|
||||
pub(crate) async fn upgrade_composefs(_opts: UpgradeOpts) -> Result<()> {
|
||||
// TODO: IMPORTANT Have all the checks here that `bootc upgrade` has for an ostree booted system
|
||||
|
||||
let host = composefs_deployment_status()
|
||||
.await
|
||||
.context("Getting composefs deployment status")?;
|
||||
|
||||
// TODO: IMPORTANT We need to check if any deployment is staged and get the image from that
|
||||
let imgref = host
|
||||
.spec
|
||||
.image
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("No image source specified"))?;
|
||||
|
||||
let (repo, entries, id, fs) = pull_composefs_repo(&imgref.transport, &imgref.image).await?;
|
||||
|
||||
let Some(entry) = entries.into_iter().next() else {
|
||||
anyhow::bail!("No boot entries!");
|
||||
};
|
||||
|
||||
let boot_type = BootType::from(&entry);
|
||||
let mut boot_digest = None;
|
||||
|
||||
match boot_type {
|
||||
BootType::Bls => {
|
||||
boot_digest = Some(setup_composefs_bls_boot(
|
||||
BootSetupType::Upgrade((&fs, &host)),
|
||||
repo,
|
||||
&id,
|
||||
entry,
|
||||
)?)
|
||||
}
|
||||
|
||||
BootType::Uki => {
|
||||
setup_composefs_uki_boot(BootSetupType::Upgrade((&fs, &host)), repo, &id, entry)?
|
||||
}
|
||||
};
|
||||
|
||||
write_composefs_state(
|
||||
&Utf8PathBuf::from("/sysroot"),
|
||||
id,
|
||||
imgref,
|
||||
true,
|
||||
boot_type,
|
||||
boot_digest,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -16,20 +16,25 @@ pub(crate) fn install_via_bootupd(
|
||||
device: &PartitionTable,
|
||||
rootfs: &Utf8Path,
|
||||
configopts: &crate::install::InstallConfigOpts,
|
||||
deployment_path: &str,
|
||||
deployment_path: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let verbose = std::env::var_os("BOOTC_BOOTLOADER_DEBUG").map(|_| "-vvvv");
|
||||
// bootc defaults to only targeting the platform boot method.
|
||||
let bootupd_opts = (!configopts.generic_image).then_some(["--update-firmware", "--auto"]);
|
||||
|
||||
let srcroot = rootfs.join(deployment_path);
|
||||
let abs_deployment_path = deployment_path.map(|v| rootfs.join(v));
|
||||
let src_root_arg = if let Some(p) = abs_deployment_path.as_deref() {
|
||||
vec!["--src-root", p.as_str()]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let devpath = device.path();
|
||||
println!("Installing bootloader via bootupd");
|
||||
Command::new("bootupctl")
|
||||
.args(["backend", "install", "--write-uuid"])
|
||||
.args(verbose)
|
||||
.args(bootupd_opts.iter().copied().flatten())
|
||||
.args(["--src-root", srcroot.as_str()])
|
||||
.args(src_root_arg)
|
||||
.args(["--device", devpath.as_str(), rootfs.as_str()])
|
||||
.log_debug()
|
||||
.run_inherited_with_cmd_context()
|
||||
|
||||
@@ -29,6 +29,11 @@ use ostree_ext::sysroot::SysrootLock;
|
||||
use schemars::schema_for;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
use crate::bootc_composefs::{
|
||||
finalize::composefs_native_finalize, rollback::composefs_rollback, status::composefs_booted,
|
||||
switch::switch_composefs, update::upgrade_composefs,
|
||||
};
|
||||
use crate::deploy::RequiredHostSpec;
|
||||
use crate::lints;
|
||||
use crate::progress_jsonl::{ProgressWriter, RawProgressFd};
|
||||
@@ -646,6 +651,8 @@ pub(crate) enum Opt {
|
||||
#[clap(subcommand)]
|
||||
#[clap(hide = true)]
|
||||
Internals(InternalsOpts),
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
ComposefsFinalizeStaged,
|
||||
}
|
||||
|
||||
/// Ensure we've entered a mount namespace, so that we can remount
|
||||
@@ -968,9 +975,7 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Implementation of the `bootc switch` CLI command.
|
||||
#[context("Switching")]
|
||||
async fn switch(opts: SwitchOpts) -> Result<()> {
|
||||
pub(crate) fn imgref_for_switch(opts: &SwitchOpts) -> Result<ImageReference> {
|
||||
let transport = ostree_container::Transport::try_from(opts.transport.as_str())?;
|
||||
let imgref = ostree_container::ImageReference {
|
||||
transport,
|
||||
@@ -979,6 +984,15 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
|
||||
let sigverify = sigpolicy_from_opt(opts.enforce_container_sigpolicy);
|
||||
let target = ostree_container::OstreeImageReference { sigverify, imgref };
|
||||
let target = ImageReference::from(target);
|
||||
|
||||
return Ok(target);
|
||||
}
|
||||
|
||||
/// Implementation of the `bootc switch` CLI command.
|
||||
#[context("Switching")]
|
||||
async fn switch(opts: SwitchOpts) -> Result<()> {
|
||||
let target = imgref_for_switch(&opts)?;
|
||||
|
||||
let prog: ProgressWriter = opts.progress.try_into()?;
|
||||
|
||||
// If we're doing an in-place mutation, we shortcut most of the rest of the work here
|
||||
@@ -1069,7 +1083,7 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
|
||||
|
||||
/// Implementation of the `bootc rollback` CLI command.
|
||||
#[context("Rollback")]
|
||||
async fn rollback(opts: RollbackOpts) -> Result<()> {
|
||||
async fn rollback(opts: &RollbackOpts) -> Result<()> {
|
||||
let sysroot = &get_storage().await?;
|
||||
let ostree = sysroot.get_ostree()?;
|
||||
crate::deploy::rollback(sysroot).await?;
|
||||
@@ -1086,10 +1100,6 @@ async fn rollback(opts: RollbackOpts) -> Result<()> {
|
||||
)?;
|
||||
}
|
||||
|
||||
if opts.apply {
|
||||
crate::reboot::reboot()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1233,9 +1243,45 @@ impl Opt {
|
||||
async fn run_from_opt(opt: Opt) -> Result<()> {
|
||||
let root = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
|
||||
match opt {
|
||||
Opt::Upgrade(opts) => upgrade(opts).await,
|
||||
Opt::Switch(opts) => switch(opts).await,
|
||||
Opt::Rollback(opts) => rollback(opts).await,
|
||||
Opt::Upgrade(opts) => {
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
if composefs_booted()?.is_some() {
|
||||
upgrade_composefs(opts).await
|
||||
} else {
|
||||
upgrade(opts).await
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "composefs-backend"))]
|
||||
upgrade(opts).await
|
||||
}
|
||||
Opt::Switch(opts) => {
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
if composefs_booted()?.is_some() {
|
||||
switch_composefs(opts).await
|
||||
} else {
|
||||
switch(opts).await
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "composefs-backend"))]
|
||||
switch(opts).await
|
||||
}
|
||||
Opt::Rollback(opts) => {
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
if composefs_booted()?.is_some() {
|
||||
composefs_rollback().await?
|
||||
} else {
|
||||
rollback(&opts).await?
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "composefs-backend"))]
|
||||
rollback(&opts).await?;
|
||||
|
||||
if opts.apply {
|
||||
crate::reboot::reboot()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Opt::Edit(opts) => edit(opts).await,
|
||||
Opt::UsrOverlay => usroverlay().await,
|
||||
Opt::Container(opts) => match opts {
|
||||
@@ -1375,8 +1421,7 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
|
||||
FsverityOpts::Enable { path } => {
|
||||
let fd =
|
||||
std::fs::File::open(&path).with_context(|| format!("Reading {path}"))?;
|
||||
// Note this is not robust to forks, we're not using the _maybe_copy variant
|
||||
fsverity::enable_verity_with_retry::<fsverity::Sha256HashValue>(&fd)?;
|
||||
fsverity::enable_verity_raw::<fsverity::Sha256HashValue>(&fd)?;
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
@@ -1477,6 +1522,9 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
Opt::ComposefsFinalizeStaged => composefs_native_finalize().await,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
33
crates/lib/src/composefs_consts.rs
Normal file
33
crates/lib/src/composefs_consts.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
/// composefs= paramter in kernel cmdline
|
||||
pub const COMPOSEFS_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 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";
|
||||
@@ -53,17 +53,21 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(feature = "install-to-disk")]
|
||||
use self::baseline::InstallBlockDeviceOpts;
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
use crate::bootc_composefs::{boot::setup_composefs_boot, repo::initialize_composefs_repository};
|
||||
use crate::boundimage::{BoundImage, ResolvedBoundImage};
|
||||
use crate::containerenv::ContainerExecutionInfo;
|
||||
use crate::deploy::{prepare_for_pull, pull_from_prepared, PreparedImportMeta, PreparedPullResult};
|
||||
use crate::lsm;
|
||||
use crate::progress_jsonl::ProgressWriter;
|
||||
use crate::spec::ImageReference;
|
||||
use crate::spec::{Bootloader, ImageReference};
|
||||
use crate::store::Storage;
|
||||
use crate::task::Task;
|
||||
use crate::utils::sigpolicy_from_opt;
|
||||
use bootc_kernel_cmdline::{bytes, utf8};
|
||||
use bootc_kernel_cmdline::{bytes, utf8, INITRD_ARG_PREFIX, ROOTFLAGS};
|
||||
use bootc_mount::Filesystem;
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
use composefs::fsverity::FsVerityHashValue;
|
||||
|
||||
/// The toplevel boot directory
|
||||
const BOOT: &str = "boot";
|
||||
@@ -81,12 +85,10 @@ const OSTREE_COMPOSEFS_SUPER: &str = ".ostree.cfs";
|
||||
/// The mount path for selinux
|
||||
const SELINUXFS: &str = "/sys/fs/selinux";
|
||||
/// The mount path for uefi
|
||||
const EFIVARFS: &str = "/sys/firmware/efi/efivars";
|
||||
pub(crate) const EFIVARFS: &str = "/sys/firmware/efi/efivars";
|
||||
pub(crate) const ARCH_USES_EFI: bool = cfg!(any(target_arch = "x86_64", target_arch = "aarch64"));
|
||||
/// This is used by dracut.
|
||||
pub const INITRD_ARG_PREFIX: &str = "rd.";
|
||||
/// The kernel argument for configuring the rootfs flags.
|
||||
pub const ROOTFLAGS: &str = "rootflags";
|
||||
pub(crate) const ESP_GUID: &str = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B";
|
||||
pub(crate) const DPS_UUID: &str = "6523f8ae-3eb1-4e2a-a05a-18b695ae656f";
|
||||
|
||||
const DEFAULT_REPO_CONFIG: &[(&str, &str)] = &[
|
||||
// Default to avoiding grub2-mkconfig etc.
|
||||
@@ -98,7 +100,7 @@ const DEFAULT_REPO_CONFIG: &[(&str, &str)] = &[
|
||||
];
|
||||
|
||||
/// Kernel argument used to specify we want the rootfs mounted read-write by default
|
||||
const RW_KARG: &str = "rw";
|
||||
pub(crate) const RW_KARG: &str = "rw";
|
||||
|
||||
#[derive(clap::Args, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub(crate) struct InstallTargetOpts {
|
||||
@@ -193,7 +195,7 @@ pub(crate) struct InstallConfigOpts {
|
||||
///
|
||||
/// Example: --karg=nosmt --karg=console=ttyS0,114800n8
|
||||
#[clap(long)]
|
||||
karg: Option<Vec<String>>,
|
||||
pub(crate) karg: Option<Vec<String>>,
|
||||
|
||||
/// The path to an `authorized_keys` that will be injected into the `root` account.
|
||||
///
|
||||
@@ -225,6 +227,17 @@ pub(crate) struct InstallConfigOpts {
|
||||
pub(crate) stateroot: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub(crate) struct InstallComposefsOpts {
|
||||
#[clap(long, default_value_t)]
|
||||
#[serde(default)]
|
||||
pub(crate) insecure: bool,
|
||||
|
||||
#[clap(long, default_value_t)]
|
||||
#[serde(default)]
|
||||
pub(crate) bootloader: Bootloader,
|
||||
}
|
||||
|
||||
#[cfg(feature = "install-to-disk")]
|
||||
#[derive(Debug, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub(crate) struct InstallToDiskOpts {
|
||||
@@ -248,6 +261,16 @@ pub(crate) struct InstallToDiskOpts {
|
||||
#[clap(long)]
|
||||
#[serde(default)]
|
||||
pub(crate) via_loopback: bool,
|
||||
|
||||
#[clap(long)]
|
||||
#[serde(default)]
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
pub(crate) composefs_native: bool,
|
||||
|
||||
#[clap(flatten)]
|
||||
#[serde(flatten)]
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
pub(crate) composefs_opts: InstallComposefsOpts,
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -370,6 +393,7 @@ pub(crate) struct SourceInfo {
|
||||
}
|
||||
|
||||
// Shared read-only global state
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct State {
|
||||
pub(crate) source: SourceInfo,
|
||||
/// Force SELinux off in target system
|
||||
@@ -387,6 +411,10 @@ pub(crate) struct State {
|
||||
/// The root filesystem of the running container
|
||||
pub(crate) container_root: Dir,
|
||||
pub(crate) tempdir: TempDir,
|
||||
|
||||
// If Some, then --composefs_native is passed
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
pub(crate) composefs_options: Option<InstallComposefsOpts>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
@@ -513,6 +541,20 @@ impl FromStr for MountSpec {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "install-to-disk", feature = "composefs-backend"))]
|
||||
impl InstallToDiskOpts {
|
||||
pub(crate) fn validate(&self) -> Result<()> {
|
||||
if !self.composefs_native {
|
||||
// Reject using --insecure without --composefs
|
||||
if self.composefs_opts.insecure != false {
|
||||
anyhow::bail!("--insecure must not be provided without --composefs");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SourceInfo {
|
||||
// Inspect container information and convert it to an ostree image reference
|
||||
// that pulls from containers-storage.
|
||||
@@ -928,17 +970,17 @@ pub(crate) fn exec_in_host_mountns(args: &[std::ffi::OsString]) -> Result<()> {
|
||||
pub(crate) struct RootSetup {
|
||||
#[cfg(feature = "install-to-disk")]
|
||||
luks_device: Option<String>,
|
||||
device_info: bootc_blockdev::PartitionTable,
|
||||
pub(crate) device_info: bootc_blockdev::PartitionTable,
|
||||
/// Absolute path to the location where we've mounted the physical
|
||||
/// root filesystem for the system we're installing.
|
||||
physical_root_path: Utf8PathBuf,
|
||||
pub(crate) physical_root_path: Utf8PathBuf,
|
||||
/// Directory file descriptor for the above physical root.
|
||||
physical_root: Dir,
|
||||
rootfs_uuid: Option<String>,
|
||||
pub(crate) physical_root: Dir,
|
||||
pub(crate) rootfs_uuid: Option<String>,
|
||||
/// True if we should skip finalizing
|
||||
skip_finalize: bool,
|
||||
boot: Option<MountSpec>,
|
||||
kargs: Vec<String>,
|
||||
pub(crate) kargs: Vec<String>,
|
||||
}
|
||||
|
||||
fn require_boot_uuid(spec: &MountSpec) -> Result<&str> {
|
||||
@@ -949,7 +991,7 @@ fn require_boot_uuid(spec: &MountSpec) -> Result<&str> {
|
||||
impl RootSetup {
|
||||
/// Get the UUID= mount specifier for the /boot filesystem; if there isn't one, the root UUID will
|
||||
/// be returned.
|
||||
fn get_boot_uuid(&self) -> Result<Option<&str>> {
|
||||
pub(crate) fn get_boot_uuid(&self) -> Result<Option<&str>> {
|
||||
self.boot.as_ref().map(require_boot_uuid).transpose()
|
||||
}
|
||||
|
||||
@@ -1158,6 +1200,7 @@ async fn prepare_install(
|
||||
config_opts: InstallConfigOpts,
|
||||
source_opts: InstallSourceOpts,
|
||||
target_opts: InstallTargetOpts,
|
||||
_composefs_opts: Option<InstallComposefsOpts>,
|
||||
) -> Result<Arc<State>> {
|
||||
tracing::trace!("Preparing install");
|
||||
let rootfs = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())
|
||||
@@ -1302,6 +1345,8 @@ async fn prepare_install(
|
||||
container_root: rootfs,
|
||||
tempdir,
|
||||
host_is_container,
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
composefs_options: _composefs_opts,
|
||||
});
|
||||
|
||||
Ok(state)
|
||||
@@ -1338,7 +1383,7 @@ async fn install_with_sysroot(
|
||||
&rootfs.device_info,
|
||||
&rootfs.physical_root_path,
|
||||
&state.config_opts,
|
||||
&deployment_path.as_str(),
|
||||
Some(&deployment_path.as_str()),
|
||||
)?;
|
||||
}
|
||||
tracing::debug!("Installed bootloader");
|
||||
@@ -1400,29 +1445,7 @@ impl BoundImages {
|
||||
}
|
||||
}
|
||||
|
||||
async fn install_to_filesystem_impl(
|
||||
state: &State,
|
||||
rootfs: &mut RootSetup,
|
||||
cleanup: Cleanup,
|
||||
) -> Result<()> {
|
||||
if matches!(state.selinux_state, SELinuxFinalState::ForceTargetDisabled) {
|
||||
rootfs.kargs.push("selinux=0".to_string());
|
||||
}
|
||||
// Drop exclusive ownership since we're done with mutation
|
||||
let rootfs = &*rootfs;
|
||||
|
||||
match &rootfs.device_info.label {
|
||||
bootc_blockdev::PartitionType::Dos => crate::utils::medium_visibility_warning(
|
||||
"Installing to `dos` format partitions is not recommended",
|
||||
),
|
||||
bootc_blockdev::PartitionType::Gpt => {
|
||||
// The only thing we should be using in general
|
||||
}
|
||||
bootc_blockdev::PartitionType::Unknown(o) => {
|
||||
crate::utils::medium_visibility_warning(&format!("Unknown partition label {o}"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn ostree_install(state: &State, rootfs: &RootSetup, cleanup: Cleanup) -> Result<()> {
|
||||
// We verify this upfront because it's currently required by bootupd
|
||||
let boot_uuid = rootfs
|
||||
.get_boot_uuid()?
|
||||
@@ -1451,7 +1474,7 @@ async fn install_to_filesystem_impl(
|
||||
if matches!(cleanup, Cleanup::TriggerOnNextBoot) {
|
||||
let sysroot_dir = crate::utils::sysroot_dir(ostree)?;
|
||||
tracing::debug!("Writing {DESTRUCTIVE_CLEANUP}");
|
||||
sysroot_dir.atomic_write(format!("etc/{DESTRUCTIVE_CLEANUP}"), b"")?;
|
||||
sysroot_dir.atomic_write(format!("etc/{}", DESTRUCTIVE_CLEANUP), b"")?;
|
||||
}
|
||||
|
||||
// We must drop the sysroot here in order to close any open file
|
||||
@@ -1461,6 +1484,46 @@ async fn install_to_filesystem_impl(
|
||||
// Run this on every install as the penultimate step
|
||||
install_finalize(&rootfs.physical_root_path).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn install_to_filesystem_impl(
|
||||
state: &State,
|
||||
rootfs: &mut RootSetup,
|
||||
cleanup: Cleanup,
|
||||
) -> Result<()> {
|
||||
if matches!(state.selinux_state, SELinuxFinalState::ForceTargetDisabled) {
|
||||
rootfs.kargs.push("selinux=0".to_string());
|
||||
}
|
||||
// Drop exclusive ownership since we're done with mutation
|
||||
let rootfs = &*rootfs;
|
||||
|
||||
match &rootfs.device_info.label {
|
||||
bootc_blockdev::PartitionType::Dos => crate::utils::medium_visibility_warning(
|
||||
"Installing to `dos` format partitions is not recommended",
|
||||
),
|
||||
bootc_blockdev::PartitionType::Gpt => {
|
||||
// The only thing we should be using in general
|
||||
}
|
||||
bootc_blockdev::PartitionType::Unknown(o) => {
|
||||
crate::utils::medium_visibility_warning(&format!("Unknown partition label {o}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
if state.composefs_options.is_some() {
|
||||
// Load a fd for the mounted target physical root
|
||||
|
||||
let (id, verity) = initialize_composefs_repository(state, rootfs).await?;
|
||||
tracing::info!("id: {}, verity: {}", hex::encode(id), verity.to_hex());
|
||||
setup_composefs_boot(rootfs, state, &hex::encode(id))?;
|
||||
} else {
|
||||
ostree_install(state, rootfs, cleanup).await?;
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "composefs-backend"))]
|
||||
ostree_install(state, rootfs, cleanup).await?;
|
||||
|
||||
// Finalize mounted filesystems
|
||||
if !rootfs.skip_finalize {
|
||||
let bootfs = rootfs.boot.as_ref().map(|_| ("boot", "boot"));
|
||||
@@ -1480,6 +1543,9 @@ fn installation_complete() {
|
||||
#[context("Installing to disk")]
|
||||
#[cfg(feature = "install-to-disk")]
|
||||
pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> {
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
opts.validate()?;
|
||||
|
||||
// Log the disk installation operation to systemd journal
|
||||
const INSTALL_DISK_JOURNAL_ID: &str = "8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2";
|
||||
let source_image = opts
|
||||
@@ -1521,7 +1587,24 @@ pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> {
|
||||
} else if !target_blockdev_meta.file_type().is_block_device() {
|
||||
anyhow::bail!("Not a block device: {}", block_opts.device);
|
||||
}
|
||||
let state = prepare_install(opts.config_opts, opts.source_opts, opts.target_opts).await?;
|
||||
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
let composefs_arg = if opts.composefs_native {
|
||||
Some(opts.composefs_opts)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "composefs-backend"))]
|
||||
let composefs_arg = None;
|
||||
|
||||
let state = prepare_install(
|
||||
opts.config_opts,
|
||||
opts.source_opts,
|
||||
opts.target_opts,
|
||||
composefs_arg,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// This is all blocking stuff
|
||||
let (mut rootfs, loopback) = {
|
||||
@@ -1752,7 +1835,7 @@ pub(crate) async fn install_to_filesystem(
|
||||
// IMPORTANT: and hence anything that is done before MUST BE IDEMPOTENT.
|
||||
// IMPORTANT: In practice, we should only be gathering information before this point,
|
||||
// IMPORTANT: and not performing any mutations at all.
|
||||
let state = prepare_install(opts.config_opts, opts.source_opts, opts.target_opts).await?;
|
||||
let state = prepare_install(opts.config_opts, opts.source_opts, opts.target_opts, None).await?;
|
||||
// And the last bit of state here is the fsopts, which we also destructure now.
|
||||
let mut fsopts = opts.filesystem_opts;
|
||||
|
||||
|
||||
@@ -42,8 +42,6 @@ pub(crate) const LINUX_PARTTYPE: &str = "0FC63DAF-8483-4772-8E79-3D69D8477DE4";
|
||||
pub(crate) const PREPBOOT_GUID: &str = "9E1A2D38-C612-4316-AA26-8B49521E5A8B";
|
||||
#[cfg(feature = "install-to-disk")]
|
||||
pub(crate) const PREPBOOT_LABEL: &str = "PowerPC-PReP-boot";
|
||||
#[cfg(feature = "install-to-disk")]
|
||||
pub(crate) const ESP_GUID: &str = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B";
|
||||
|
||||
#[derive(clap::ValueEnum, Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
@@ -106,10 +104,15 @@ fn mkfs<'a>(
|
||||
label: &str,
|
||||
wipe: bool,
|
||||
opts: impl IntoIterator<Item = &'a str>,
|
||||
dps_uuid: Option<uuid::Uuid>,
|
||||
) -> Result<uuid::Uuid> {
|
||||
let devinfo = bootc_blockdev::list_dev(dev.into())?;
|
||||
let size = ostree_ext::glib::format_size(devinfo.size);
|
||||
let u = uuid::Uuid::new_v4();
|
||||
let u = if let Some(u) = dps_uuid {
|
||||
u
|
||||
} else {
|
||||
uuid::Uuid::new_v4()
|
||||
};
|
||||
let mut t = Task::new(
|
||||
&format!("Creating {label} filesystem ({fs}) on device {dev} (size={size})"),
|
||||
format!("mkfs.{fs}"),
|
||||
@@ -275,7 +278,7 @@ pub(crate) fn install_create_rootfs(
|
||||
}
|
||||
|
||||
let esp_partno = if super::ARCH_USES_EFI {
|
||||
let esp_guid = ESP_GUID;
|
||||
let esp_guid = crate::install::ESP_GUID;
|
||||
partno += 1;
|
||||
writeln!(
|
||||
&mut partitioning_buf,
|
||||
@@ -383,6 +386,7 @@ pub(crate) fn install_create_rootfs(
|
||||
"boot",
|
||||
opts.wipe,
|
||||
[],
|
||||
None,
|
||||
)
|
||||
.context("Initializing /boot")?,
|
||||
)
|
||||
@@ -403,6 +407,8 @@ pub(crate) fn install_create_rootfs(
|
||||
"root",
|
||||
opts.wipe,
|
||||
mkfs_options.iter().copied(),
|
||||
// TODO: Add cli option for this
|
||||
Some(uuid::uuid!(crate::install::DPS_UUID)),
|
||||
)?;
|
||||
let rootarg = format!("root=UUID={root_uuid}");
|
||||
let bootsrc = boot_uuid.as_ref().map(|uuid| format!("UUID={uuid}"));
|
||||
@@ -418,6 +424,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)?;
|
||||
|
||||
@@ -4,20 +4,27 @@
|
||||
//! to provide a fully "container native" tool for using
|
||||
//! bootable container images.
|
||||
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
mod bootc_composefs;
|
||||
pub(crate) mod bootc_kargs;
|
||||
mod bootloader;
|
||||
mod boundimage;
|
||||
mod cfsctl;
|
||||
pub mod cli;
|
||||
mod composefs_consts;
|
||||
mod containerenv;
|
||||
pub(crate) mod deploy;
|
||||
pub(crate) mod fsck;
|
||||
pub(crate) mod generator;
|
||||
mod glyph;
|
||||
mod image;
|
||||
mod install;
|
||||
pub(crate) mod journal;
|
||||
mod k8sapitypes;
|
||||
mod lints;
|
||||
mod lsm;
|
||||
pub(crate) mod metadata;
|
||||
mod parsers;
|
||||
mod podman;
|
||||
mod podstorage;
|
||||
mod progress_jsonl;
|
||||
@@ -31,13 +38,6 @@ mod utils;
|
||||
#[cfg(feature = "docgen")]
|
||||
mod cli_json;
|
||||
|
||||
mod bootloader;
|
||||
mod containerenv;
|
||||
mod install;
|
||||
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) mod parsers;
|
||||
#[cfg(feature = "rhsm")]
|
||||
mod rhsm;
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
//!
|
||||
//! This module parses the config files for the spec.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
@@ -12,7 +14,7 @@ use uapi_version::Version;
|
||||
/// The boot loader should present the available boot menu entries to the user in a sorted list.
|
||||
/// The list should be sorted by the `sort-key` field, if it exists, otherwise by the `machine-id` field.
|
||||
/// If multiple entries have the same `sort-key` (or `machine-id`), they should be sorted by the `version` field in descending order.
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
#[derive(Debug, Eq, PartialEq, Default)]
|
||||
#[non_exhaustive]
|
||||
pub(crate) struct BLSConfig {
|
||||
/// The title of the boot entry, to be displayed in the boot menu.
|
||||
@@ -103,6 +105,41 @@ impl BLSConfig {
|
||||
pub(crate) fn version(&self) -> Version {
|
||||
Version::from(&self.version)
|
||||
}
|
||||
|
||||
pub(crate) fn with_title(&mut self, new_val: String) -> &mut Self {
|
||||
self.title = Some(new_val);
|
||||
self
|
||||
}
|
||||
pub(crate) fn with_version(&mut self, new_val: String) -> &mut Self {
|
||||
self.version = new_val;
|
||||
self
|
||||
}
|
||||
pub(crate) fn with_linux(&mut self, new_val: String) -> &mut Self {
|
||||
self.linux = new_val;
|
||||
self
|
||||
}
|
||||
pub(crate) fn with_initrd(&mut self, new_val: Vec<String>) -> &mut Self {
|
||||
self.initrd = new_val;
|
||||
self
|
||||
}
|
||||
pub(crate) fn with_options(&mut self, new_val: String) -> &mut Self {
|
||||
self.options = Some(new_val);
|
||||
self
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn with_machine_id(&mut self, new_val: String) -> &mut Self {
|
||||
self.machine_id = Some(new_val);
|
||||
self
|
||||
}
|
||||
pub(crate) fn with_sort_key(&mut self, new_val: String) -> &mut Self {
|
||||
self.sort_key = Some(new_val);
|
||||
self
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn with_extra(&mut self, new_val: HashMap<String, String>) -> &mut Self {
|
||||
self.extra = new_val;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_bls_config(input: &str) -> Result<BLSConfig> {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//! Parser for GRUB menuentry configuration files using nom combinators.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
use nom::{
|
||||
@@ -14,13 +16,15 @@ use nom::{
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) struct MenuentryBody<'a> {
|
||||
/// Kernel modules to load
|
||||
insmod: Vec<&'a str>,
|
||||
pub(crate) insmod: Vec<&'a str>,
|
||||
/// Chainloader path (optional)
|
||||
chainloader: Option<&'a str>,
|
||||
pub(crate) chainloader: String,
|
||||
/// Search command (optional)
|
||||
search: Option<&'a str>,
|
||||
pub(crate) search: &'a str,
|
||||
/// The version
|
||||
pub(crate) version: u8,
|
||||
/// Additional commands
|
||||
extra: Vec<(&'a str, &'a str)>,
|
||||
pub(crate) extra: Vec<(&'a str, &'a str)>,
|
||||
}
|
||||
|
||||
impl<'a> Display for MenuentryBody<'a> {
|
||||
@@ -29,13 +33,8 @@ impl<'a> Display for MenuentryBody<'a> {
|
||||
writeln!(f, "insmod {}", insmod)?;
|
||||
}
|
||||
|
||||
if let Some(search) = self.search {
|
||||
writeln!(f, "search {}", search)?;
|
||||
}
|
||||
|
||||
if let Some(chainloader) = self.chainloader {
|
||||
writeln!(f, "chainloader {}", chainloader)?;
|
||||
}
|
||||
writeln!(f, "search {}", self.search)?;
|
||||
writeln!(f, "chainloader {}", self.chainloader)?;
|
||||
|
||||
for (k, v) in &self.extra {
|
||||
writeln!(f, "{k} {v}")?;
|
||||
@@ -49,17 +48,17 @@ 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: None,
|
||||
search: None,
|
||||
chainloader: "".into(),
|
||||
search: "",
|
||||
version: 0,
|
||||
extra: vec![],
|
||||
};
|
||||
|
||||
for (key, value) in vec {
|
||||
match key {
|
||||
"insmod" => entry.insmod.push(value),
|
||||
"chainloader" => entry.chainloader = Some(value),
|
||||
"search" => entry.search = Some(value),
|
||||
// Skip 'set' commands as they are typically variable assignments
|
||||
"chainloader" => entry.chainloader = value.into(),
|
||||
"search" => entry.search = value,
|
||||
"set" => {}
|
||||
_ => entry.extra.push((key, value)),
|
||||
}
|
||||
@@ -73,7 +72,7 @@ impl<'a> From<Vec<(&'a str, &'a str)>> for MenuentryBody<'a> {
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) struct MenuEntry<'a> {
|
||||
/// Display title (supports escaped quotes)
|
||||
pub(crate) title: &'a str,
|
||||
pub(crate) title: String,
|
||||
/// Commands within the menuentry block
|
||||
pub(crate) body: MenuentryBody<'a>,
|
||||
}
|
||||
@@ -86,6 +85,22 @@ impl<'a> Display for MenuEntry<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MenuEntry<'a> {
|
||||
#[allow(dead_code)]
|
||||
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![],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parser that takes content until balanced brackets, handling nested brackets and escapes.
|
||||
fn take_until_balanced_allow_nested(
|
||||
opening_bracket: char,
|
||||
@@ -180,7 +195,7 @@ fn parse_menuentry(input: &str) -> IResult<&str, MenuEntry<'_>> {
|
||||
Ok((
|
||||
input,
|
||||
MenuEntry {
|
||||
title,
|
||||
title: title.to_string(),
|
||||
body: MenuentryBody::from(map),
|
||||
},
|
||||
))
|
||||
@@ -272,20 +287,22 @@ mod test {
|
||||
|
||||
let expected = vec![
|
||||
MenuEntry {
|
||||
title: "Fedora 42: (Verity-42)",
|
||||
title: "Fedora 42: (Verity-42)".into(),
|
||||
body: MenuentryBody {
|
||||
insmod: vec!["fat", "chain"],
|
||||
search: Some("--no-floppy --set=root --fs-uuid \"${EFI_PART_UUID}\""),
|
||||
chainloader: Some("/EFI/Linux/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6.efi"),
|
||||
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)",
|
||||
title: "Fedora 43: (Verity-43)".into(),
|
||||
body: MenuentryBody {
|
||||
insmod: vec!["fat", "chain"],
|
||||
search: Some("--no-floppy --set=root --fs-uuid \"${EFI_PART_UUID}\""),
|
||||
chainloader: Some("/EFI/Linux/uki.efi"),
|
||||
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")
|
||||
@@ -312,7 +329,7 @@ mod test {
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].title, "Title with \\\"escaped quotes\\\" inside");
|
||||
assert_eq!(result[0].body.chainloader, Some("/EFI/Linux/test.efi"));
|
||||
assert_eq!(result[0].body.chainloader, "/EFI/Linux/test.efi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -361,8 +378,8 @@ mod test {
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].title, "Minimal Entry");
|
||||
assert_eq!(result[0].body.insmod.len(), 0);
|
||||
assert_eq!(result[0].body.chainloader, None);
|
||||
assert_eq!(result[0].body.search, None);
|
||||
assert_eq!(result[0].body.chainloader, "");
|
||||
assert_eq!(result[0].body.search, "");
|
||||
assert_eq!(result[0].body.extra.len(), 0);
|
||||
}
|
||||
|
||||
@@ -380,8 +397,8 @@ mod test {
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].body.insmod, vec!["fat", "chain", "ext2"]);
|
||||
assert_eq!(result[0].body.chainloader, None);
|
||||
assert_eq!(result[0].body.search, None);
|
||||
assert_eq!(result[0].body.chainloader, "");
|
||||
assert_eq!(result[0].body.search, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -399,7 +416,7 @@ mod test {
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].body.insmod, vec!["fat"]);
|
||||
assert_eq!(result[0].body.chainloader, Some("/EFI/Linux/test.efi"));
|
||||
assert_eq!(result[0].body.chainloader, "/EFI/Linux/test.efi");
|
||||
// set commands should be ignored
|
||||
assert!(!result[0].body.extra.iter().any(|(k, _)| k == &"set"));
|
||||
}
|
||||
@@ -421,7 +438,7 @@ mod test {
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].title, "Nested Braces");
|
||||
assert_eq!(result[0].body.insmod, vec!["fat"]);
|
||||
assert_eq!(result[0].body.chainloader, Some("/EFI/Linux/test.efi"));
|
||||
assert_eq!(result[0].body.chainloader, "/EFI/Linux/test.efi");
|
||||
// The if/fi block should be captured as extra commands
|
||||
assert!(result[0].body.extra.iter().any(|(k, _)| k == &"if"));
|
||||
}
|
||||
@@ -500,12 +517,9 @@ mod test {
|
||||
|
||||
assert_eq!(result.len(), 2);
|
||||
assert_eq!(result[0].title, "First Entry");
|
||||
assert_eq!(result[0].body.chainloader, Some("/EFI/Linux/first.efi"));
|
||||
assert_eq!(result[0].body.chainloader, "/EFI/Linux/first.efi");
|
||||
assert_eq!(result[1].title, "Second Entry");
|
||||
assert_eq!(result[1].body.chainloader, Some("/EFI/Linux/second.efi"));
|
||||
assert_eq!(
|
||||
result[1].body.search,
|
||||
Some("--set=root --fs-uuid \"some-uuid\"")
|
||||
);
|
||||
assert_eq!(result[1].body.chainloader, "/EFI/Linux/second.efi");
|
||||
assert_eq!(result[1].body.search, "--set=root --fs-uuid \"some-uuid\"");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! The definition for host system state.
|
||||
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
use ostree_ext::container::Transport;
|
||||
@@ -10,6 +11,8 @@ use ostree_ext::{container::OstreeImageReference, oci_spec};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
use crate::bootc_composefs::boot::BootType;
|
||||
use crate::{k8sapitypes, status::Slot};
|
||||
|
||||
const API_VERSION: &str = "org.containers.bootc/v1";
|
||||
@@ -160,6 +163,52 @@ pub struct BootEntryOstree {
|
||||
pub deploy_serial: u32,
|
||||
}
|
||||
|
||||
/// Bootloader type to determine whether system was booted via Grub or Systemd
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
pub enum Bootloader {
|
||||
/// Booted via Grub
|
||||
#[default]
|
||||
Grub,
|
||||
/// Booted via Systemd
|
||||
Systemd,
|
||||
}
|
||||
|
||||
impl Display for Bootloader {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let string = match self {
|
||||
Bootloader::Grub => "grub",
|
||||
Bootloader::Systemd => "systemd",
|
||||
};
|
||||
|
||||
write!(f, "{}", string)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Bootloader {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self> {
|
||||
match value {
|
||||
"grub" => Ok(Self::Grub),
|
||||
"systemd" => Ok(Self::Systemd),
|
||||
unrecognized => Err(anyhow::anyhow!("Unrecognized bootloader: '{unrecognized}'")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A bootable entry
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
pub struct BootEntryComposefs {
|
||||
/// The erofs verity
|
||||
pub verity: String,
|
||||
/// Whether this deployment is to be booted via Type1 (vmlinuz + initrd) or Type2 (UKI) entry
|
||||
pub boot_type: BootType,
|
||||
/// Whether we boot using systemd or grub
|
||||
pub bootloader: Bootloader,
|
||||
}
|
||||
|
||||
/// A bootable entry
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -180,6 +229,9 @@ pub struct BootEntry {
|
||||
pub store: Option<Store>,
|
||||
/// If this boot entry is ostree based, the corresponding state
|
||||
pub ostree: Option<BootEntryOstree>,
|
||||
/// If this boot entry is composefs based, the corresponding state
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
pub composefs: Option<BootEntryComposefs>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
@@ -250,6 +302,20 @@ impl Host {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
pub(crate) fn require_composefs_booted(&self) -> anyhow::Result<&BootEntryComposefs> {
|
||||
let cfs = self
|
||||
.status
|
||||
.booted
|
||||
.as_ref()
|
||||
.ok_or(anyhow::anyhow!("Could not find booted deployment"))?
|
||||
.composefs
|
||||
.as_ref()
|
||||
.ok_or(anyhow::anyhow!("Could not find booted image"))?;
|
||||
|
||||
Ok(cfs)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Host {
|
||||
@@ -520,6 +586,8 @@ mod tests {
|
||||
pinned: false,
|
||||
store: None,
|
||||
ostree: None,
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
composefs: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,12 @@ use ostree_ext::keyfileext::KeyFileExt;
|
||||
use ostree_ext::oci_spec;
|
||||
use ostree_ext::oci_spec::image::Digest;
|
||||
use ostree_ext::oci_spec::image::ImageConfiguration;
|
||||
use ostree_ext::ostree;
|
||||
use ostree_ext::sysroot::SysrootLock;
|
||||
|
||||
use ostree_ext::ostree;
|
||||
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
use crate::bootc_composefs::status::{composefs_booted, composefs_deployment_status};
|
||||
use crate::cli::OutputFormat;
|
||||
use crate::spec::ImageStatus;
|
||||
use crate::spec::{BootEntry, BootOrder, Host, HostSpec, HostStatus, HostType};
|
||||
@@ -207,6 +210,8 @@ fn boot_entry_from_deployment(
|
||||
deploy_serial: deployment.deployserial().try_into().unwrap(),
|
||||
stateroot: deployment.stateroot().into(),
|
||||
}),
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
composefs: None,
|
||||
};
|
||||
Ok(r)
|
||||
}
|
||||
@@ -335,6 +340,38 @@ pub(crate) fn get_status(
|
||||
Ok((deployments, host))
|
||||
}
|
||||
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
async fn get_host() -> Result<Host> {
|
||||
let host = if ostree_booted()? {
|
||||
let sysroot = super::cli::get_storage().await?;
|
||||
let ostree = sysroot.get_ostree()?;
|
||||
let booted_deployment = ostree.booted_deployment();
|
||||
let (_deployments, host) = get_status(&ostree, booted_deployment.as_ref())?;
|
||||
host
|
||||
} else if composefs_booted()?.is_some() {
|
||||
composefs_deployment_status().await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
Ok(host)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "composefs-backend"))]
|
||||
async fn get_host() -> Result<Host> {
|
||||
let host = if ostree_booted()? {
|
||||
let sysroot = super::cli::get_storage().await?;
|
||||
let ostree = sysroot.get_ostree()?;
|
||||
let booted_deployment = ostree.booted_deployment();
|
||||
let (_deployments, host) = get_status(&ostree, booted_deployment.as_ref())?;
|
||||
host
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
Ok(host)
|
||||
}
|
||||
|
||||
/// Implementation of the `bootc status` CLI command.
|
||||
#[context("Status")]
|
||||
pub(crate) async fn status(opts: super::cli::StatusOpts) -> Result<()> {
|
||||
@@ -343,15 +380,7 @@ pub(crate) async fn status(opts: super::cli::StatusOpts) -> Result<()> {
|
||||
0 | 1 => {}
|
||||
o => anyhow::bail!("Unsupported format version: {o}"),
|
||||
};
|
||||
let mut host = if !ostree_booted()? {
|
||||
Default::default()
|
||||
} else {
|
||||
let sysroot = super::cli::get_storage().await?;
|
||||
let ostree = sysroot.get_ostree()?;
|
||||
let booted_deployment = ostree.booted_deployment();
|
||||
let (_deployments, host) = get_status(&ostree, booted_deployment.as_ref())?;
|
||||
host
|
||||
};
|
||||
let mut host = get_host().await?;
|
||||
|
||||
// We could support querying the staged or rollback deployments
|
||||
// here too, but it's not a common use case at the moment.
|
||||
@@ -485,6 +514,13 @@ fn human_render_slot(
|
||||
let digest = &image.image_digest;
|
||||
writeln!(out, "{digest} ({arch})")?;
|
||||
|
||||
// Write the EROFS verity if present
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
if let Some(composefs) = &entry.composefs {
|
||||
write_row_name(&mut out, "Verity", prefix_len)?;
|
||||
writeln!(out, "{}", composefs.verity)?;
|
||||
}
|
||||
|
||||
// Format the timestamp without nanoseconds since those are just irrelevant noise for human
|
||||
// consumption - that time scale should basically never matter for container builds.
|
||||
let timestamp = image
|
||||
@@ -585,6 +621,28 @@ fn human_render_slot_ostree(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Output a rendering of a non-container composefs boot entry.
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
fn human_render_slot_composefs(
|
||||
mut out: impl Write,
|
||||
slot: Slot,
|
||||
entry: &crate::spec::BootEntry,
|
||||
erofs_verity: &str,
|
||||
) -> Result<()> {
|
||||
// TODO consider rendering more ostree stuff here like rpm-ostree status does
|
||||
let prefix = match slot {
|
||||
Slot::Staged => " Staged composefs".into(),
|
||||
Slot::Booted => format!("{} Booted composefs", crate::glyph::Glyph::BlackCircle),
|
||||
Slot::Rollback => " Rollback composefs".into(),
|
||||
};
|
||||
let prefix_len = prefix.len();
|
||||
writeln!(out, "{prefix}")?;
|
||||
write_row_name(&mut out, "Commit", prefix_len)?;
|
||||
writeln!(out, "{erofs_verity}")?;
|
||||
tracing::debug!("pinned={}", entry.pinned);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn human_readable_output_booted(mut out: impl Write, host: &Host, verbose: bool) -> Result<()> {
|
||||
let mut first = true;
|
||||
for (slot_name, status) in [
|
||||
@@ -598,6 +656,25 @@ fn human_readable_output_booted(mut out: impl Write, host: &Host, verbose: bool)
|
||||
} else {
|
||||
writeln!(out)?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
if let Some(image) = &host_status.image {
|
||||
human_render_slot(&mut out, Some(slot_name), host_status, image, verbose)?;
|
||||
} else if let Some(ostree) = host_status.ostree.as_ref() {
|
||||
human_render_slot_ostree(
|
||||
&mut out,
|
||||
Some(slot_name),
|
||||
host_status,
|
||||
&ostree.checksum,
|
||||
verbose,
|
||||
)?;
|
||||
} else if let Some(composefs) = &host_status.composefs {
|
||||
human_render_slot_composefs(&mut out, slot_name, host_status, &composefs.verity)?;
|
||||
} else {
|
||||
writeln!(out, "Current {slot_name} state is unknown")?;
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "composefs-backend"))]
|
||||
if let Some(image) = &host_status.image {
|
||||
human_render_slot(&mut out, Some(slot_name), host_status, image, verbose)?;
|
||||
} else if let Some(ostree) = host_status.ostree.as_ref() {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::future::Future;
|
||||
use std::io::Write;
|
||||
use std::os::fd::BorrowedFd;
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -186,6 +188,29 @@ 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.
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
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 +248,22 @@ mod tests {
|
||||
SignatureSource::ContainerPolicyAllowInsecure
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "composefs-backend")]
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ libc = { workspace = true }
|
||||
rustix = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tracing = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
cap-std-ext = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
indoc = { workspace = true }
|
||||
|
||||
@@ -22,6 +22,8 @@ use rustix::{
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
pub mod tempmount;
|
||||
|
||||
/// Well known identifier for pid 1
|
||||
pub const PID1: Pid = const {
|
||||
match Pid::from_raw(1) {
|
||||
|
||||
76
crates/mount/src/tempmount.rs
Normal file
76
crates/mount/src/tempmount.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use std::os::fd::AsFd;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use camino::Utf8Path;
|
||||
use cap_std_ext::cap_std::{ambient_authority, fs::Dir};
|
||||
use fn_error_context::context;
|
||||
use rustix::mount::{move_mount, unmount, MoveMountFlags, UnmountFlags};
|
||||
|
||||
pub struct TempMount {
|
||||
pub dir: tempfile::TempDir,
|
||||
pub fd: Dir,
|
||||
}
|
||||
|
||||
impl TempMount {
|
||||
/// Mount device/partition on a tempdir which will be automatically unmounted on drop
|
||||
#[context("Mounting {dev}")]
|
||||
pub fn mount_dev(dev: &str) -> Result<Self> {
|
||||
let tempdir = tempfile::TempDir::new()?;
|
||||
|
||||
let utf8path = Utf8Path::from_path(tempdir.path())
|
||||
.ok_or(anyhow::anyhow!("Failed to convert path to UTF-8 Path"))?;
|
||||
|
||||
crate::mount(dev, utf8path)?;
|
||||
|
||||
let fd = Dir::open_ambient_dir(tempdir.path(), ambient_authority())
|
||||
.with_context(|| format!("Opening {:?}", tempdir.path()));
|
||||
|
||||
let fd = match fd {
|
||||
Ok(fd) => fd,
|
||||
Err(e) => {
|
||||
unmount(tempdir.path(), UnmountFlags::DETACH)?;
|
||||
Err(e)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self { dir: tempdir, fd })
|
||||
}
|
||||
|
||||
/// Mount and fd acquired with `open_tree` like syscall
|
||||
#[context("Mounting fd")]
|
||||
pub fn mount_fd(mnt_fd: impl AsFd) -> Result<Self> {
|
||||
let tempdir = tempfile::TempDir::new()?;
|
||||
|
||||
move_mount(
|
||||
mnt_fd.as_fd(),
|
||||
"",
|
||||
rustix::fs::CWD,
|
||||
tempdir.path(),
|
||||
MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH,
|
||||
)
|
||||
.context("move_mount")?;
|
||||
|
||||
let fd = Dir::open_ambient_dir(tempdir.path(), ambient_authority())
|
||||
.with_context(|| format!("Opening {:?}", tempdir.path()));
|
||||
|
||||
let fd = match fd {
|
||||
Ok(fd) => fd,
|
||||
Err(e) => {
|
||||
unmount(tempdir.path(), UnmountFlags::DETACH)?;
|
||||
Err(e)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self { dir: tempdir, fd })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempMount {
|
||||
fn drop(&mut self) {
|
||||
match unmount(self.dir.path(), UnmountFlags::DETACH) {
|
||||
Ok(_) => {}
|
||||
Err(e) => tracing::warn!("Failed to unmount tempdir: {e:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@
|
||||
// "Dependencies are re-exported". Users will need e.g. `gio::File`, so this avoids
|
||||
// them needing to update matching versions.
|
||||
pub use composefs;
|
||||
pub use composefs_boot;
|
||||
pub use composefs_oci;
|
||||
pub use containers_image_proxy;
|
||||
pub use containers_image_proxy::oci_spec;
|
||||
pub use ostree;
|
||||
|
||||
46
systemd/composefs-finalize-staged.service
Normal file
46
systemd/composefs-finalize-staged.service
Normal file
@@ -0,0 +1,46 @@
|
||||
# Copyright (C) 2018 Red Hat, Inc.
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# For some implementation discussion, see:
|
||||
# https://lists.freedesktop.org/archives/systemd-devel/2018-March/040557.html
|
||||
[Unit]
|
||||
Description=Composefs Finalize Staged Deployment
|
||||
Documentation=man:bootc(1)
|
||||
DefaultDependencies=no
|
||||
|
||||
RequiresMountsFor=/sysroot
|
||||
After=local-fs.target
|
||||
Before=basic.target final.target
|
||||
# We want to make sure the transaction logs are persisted to disk:
|
||||
# https://bugzilla.redhat.com/show_bug.cgi?id=1751272
|
||||
After=systemd-journal-flush.service
|
||||
Conflicts=final.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStop=/usr/bin/bootc composefs-finalize-staged
|
||||
# This is a quite long timeout intentionally; the failure mode
|
||||
# here is that people don't get an upgrade. We need to handle
|
||||
# cases with slow rotational media, etc.
|
||||
TimeoutStopSec=5m
|
||||
# Bootc should never touch /var at all...except, we need to remove
|
||||
# the /var/.updated flag, so we can't just `InaccessiblePaths=/var` right now.
|
||||
# For now, let's at least use ProtectHome just so we have some sandboxing
|
||||
# of that.
|
||||
ProtectHome=yes
|
||||
# And we shouldn't affect the current deployment's /etc.
|
||||
ReadOnlyPaths=/etc
|
||||
# We write to /sysroot and /boot of course.
|
||||
Reference in New Issue
Block a user