Skip to content

Commit 25602da

Browse files
cli/composefs: Handle rollback for composefs
This commit does not handle the edge cases that arise with rollbacks when there is a staged deployment present. For Grub BLS case, we create a new `loader/entries.staged` directory, write the new boot entries, then atomically swap `loader/entries` and `loader/entries.staged`. For Grub UKI case, we regenerate `grub2/user.cfg` using the images present in `/sysroot/state/deploy/` To distinguish whether the currently booted system is booted with a UKI or BLS, we add an entry to origin file called `boot_type` Signed-off-by: Johan-Liebert1 <[email protected]>
1 parent 005ce77 commit 25602da

File tree

6 files changed

+259
-49
lines changed

6 files changed

+259
-49
lines changed

lib/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ uuid = { version = "1.8.0", features = ["v4"] }
5454
tini = "1.3.0"
5555
comfy-table = "7.1.1"
5656
thiserror = { workspace = true }
57+
openat = "0.1.21"
58+
openat-ext = "0.2.3"
5759

5860
[dev-dependencies]
5961
similar-asserts = { workspace = true }

lib/src/cli.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use ostree_ext::ostree;
2525
use schemars::schema_for;
2626
use serde::{Deserialize, Serialize};
2727

28-
use crate::deploy::RequiredHostSpec;
28+
use crate::deploy::{composefs_rollback, RequiredHostSpec};
2929
use crate::install::{
3030
pull_composefs_repo, setup_composefs_bls_boot, setup_composefs_uki_boot, write_composefs_state,
3131
BootSetupType, BootType,
@@ -949,8 +949,7 @@ async fn switch_composefs(opts: SwitchOpts) -> Result<()> {
949949
anyhow::bail!("Target image is undefined")
950950
};
951951

952-
let (repo, entries, id) =
953-
pull_composefs_repo(&target_imgref.transport, &target_imgref.image).await?;
952+
let (repo, entries, id) = pull_composefs_repo(&"docker".into(), &target_imgref.image).await?;
954953

955954
let Some(entry) = entries.into_iter().next() else {
956955
anyhow::bail!("No boot entries!");
@@ -1043,8 +1042,12 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
10431042
/// Implementation of the `bootc rollback` CLI command.
10441043
#[context("Rollback")]
10451044
async fn rollback(opts: RollbackOpts) -> Result<()> {
1046-
let sysroot = &get_storage().await?;
1047-
crate::deploy::rollback(sysroot).await?;
1045+
if composefs_booted()? {
1046+
composefs_rollback().await?
1047+
} else {
1048+
let sysroot = &get_storage().await?;
1049+
crate::deploy::rollback(sysroot).await?;
1050+
};
10481051

10491052
if opts.apply {
10501053
crate::reboot::reboot()?;

lib/src/deploy.rs

Lines changed: 167 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
//! Create a merged filesystem tree with the image and mounted configmaps.
44
55
use std::collections::HashSet;
6+
use std::fs::create_dir_all;
67
use std::io::{BufRead, Write};
8+
use std::path::PathBuf;
79

810
use anyhow::Ok;
911
use anyhow::{anyhow, Context, Result};
@@ -21,13 +23,17 @@ use ostree_ext::ostree::{self, Sysroot};
2123
use ostree_ext::sysroot::SysrootLock;
2224
use ostree_ext::tokio_util::spawn_blocking_cancellable_flatten;
2325

26+
use crate::bls_config::{parse_bls_config, BLSConfig};
27+
use crate::install::{get_efi_uuid_source, get_user_config, BootType};
2428
use crate::progress_jsonl::{Event, ProgressWriter, SubTaskBytes, SubTaskStep};
2529
use crate::spec::ImageReference;
26-
use crate::spec::{BootOrder, HostSpec};
27-
use crate::status::labels_of_config;
30+
use crate::spec::{BootOrder, HostSpec, BootEntry};
31+
use crate::status::{composefs_deployment_status, labels_of_config};
2832
use crate::store::Storage;
2933
use crate::utils::async_task_with_spinner;
3034

35+
use openat_ext::OpenatDirExt;
36+
3137
// TODO use https://github.com/ostreedev/ostree-rs-ext/pull/493/commits/afc1837ff383681b947de30c0cefc70080a4f87a
3238
const BASE_IMAGE_PREFIX: &str = "ostree/container/baseimage/bootc";
3339

@@ -740,6 +746,165 @@ pub(crate) async fn stage(
740746
Ok(())
741747
}
742748

749+
750+
#[context("Rolling back UKI")]
751+
pub(crate) fn rollback_composefs_uki(current: &BootEntry, rollback: &BootEntry) -> Result<()> {
752+
let user_cfg_name = "grub2/user.cfg.staged";
753+
let user_cfg_path = PathBuf::from("/sysroot/boot").join(user_cfg_name);
754+
755+
let efi_uuid_source = get_efi_uuid_source();
756+
757+
// TODO: Need to check if user.cfg.staged exists
758+
let mut usr_cfg = std::fs::OpenOptions::new()
759+
.write(true)
760+
.create(true)
761+
.truncate(true)
762+
.open(user_cfg_path)
763+
.with_context(|| format!("Opening {user_cfg_name}"))?;
764+
765+
usr_cfg.write(efi_uuid_source.as_bytes())?;
766+
767+
let verity = if let Some(composefs) = &rollback.composefs {
768+
composefs.verity.clone()
769+
} else {
770+
// Shouldn't really happen
771+
anyhow::bail!("Verity not found for rollback deployment")
772+
};
773+
usr_cfg.write(get_user_config(&verity).as_bytes())?;
774+
775+
let verity = if let Some(composefs) = &current.composefs {
776+
composefs.verity.clone()
777+
} else {
778+
// Shouldn't really happen
779+
anyhow::bail!("Verity not found for booted deployment")
780+
};
781+
usr_cfg.write(get_user_config(&verity).as_bytes())?;
782+
783+
Ok(())
784+
}
785+
786+
/// Filename for `loader/entries`
787+
const CURRENT_ENTRIES: &str = "entries";
788+
const ROLLBACK_ENTRIES: &str = "entries.staged";
789+
790+
#[context("Getting boot entries")]
791+
pub(crate) fn get_sorted_boot_entries(ascending: bool) -> Result<Vec<BLSConfig>> {
792+
let mut all_configs = vec![];
793+
794+
for entry in std::fs::read_dir(format!("/sysroot/boot/loader/{CURRENT_ENTRIES}"))? {
795+
let entry = entry?;
796+
797+
let file_name = entry.file_name();
798+
799+
let file_name = file_name
800+
.to_str()
801+
.ok_or(anyhow::anyhow!("Found non UTF-8 characters in filename"))?;
802+
803+
if !file_name.ends_with(".conf") {
804+
continue;
805+
}
806+
807+
let contents = std::fs::read_to_string(&entry.path())
808+
.with_context(|| format!("Failed to read {:?}", entry.path()))?;
809+
810+
let config = parse_bls_config(&contents).context("Parsing bls config")?;
811+
812+
all_configs.push(config);
813+
}
814+
815+
all_configs.sort_by(|a, b| if ascending { a.cmp(b) } else { b.cmp(a) });
816+
817+
return Ok(all_configs);
818+
}
819+
820+
#[context("Rolling back BLS")]
821+
pub(crate) fn rollback_composefs_bls() -> Result<()> {
822+
// Sort in descending order as that's the order they're shown on the boot screen
823+
// After this:
824+
// all_configs[0] -> booted depl
825+
// all_configs[1] -> rollback depl
826+
let mut all_configs = get_sorted_boot_entries(false)?;
827+
828+
// Update the indicies so that they're swapped
829+
for (idx, cfg) in all_configs.iter_mut().enumerate() {
830+
cfg.version = idx as u32;
831+
}
832+
833+
assert!(all_configs.len() == 2);
834+
835+
// Write these
836+
let dir_path = PathBuf::from(format!("/sysroot/boot/loader/{ROLLBACK_ENTRIES}"));
837+
create_dir_all(&dir_path).with_context(|| format!("Failed to create dir: {dir_path:?}"))?;
838+
839+
// Write the BLS configs in there
840+
for cfg in all_configs {
841+
let file_name = format!("bootc-composefs-{}.conf", cfg.version);
842+
843+
let mut file = std::fs::OpenOptions::new()
844+
.create(true)
845+
.write(true)
846+
.open(dir_path.join(&file_name))
847+
.with_context(|| format!("Opening {file_name}"))?;
848+
849+
file.write_all(cfg.to_string().as_bytes())
850+
.with_context(|| format!("Writing to {file_name}"))?;
851+
}
852+
853+
// Atomically exchange "entries" <-> "entries.rollback"
854+
let dir = openat::Dir::open("/sysroot/boot/loader").context("Opening loader dir")?;
855+
856+
tracing::debug!("Atomically exchanging for {ROLLBACK_ENTRIES} and {CURRENT_ENTRIES}");
857+
dir.local_exchange(ROLLBACK_ENTRIES, CURRENT_ENTRIES)
858+
.context("local exchange")?;
859+
860+
tracing::debug!("Removing {ROLLBACK_ENTRIES}");
861+
dir.remove_all(ROLLBACK_ENTRIES)
862+
.context("Removing entries.rollback")?;
863+
864+
tracing::debug!("Syncing to disk");
865+
dir.syncfs().context("syncfs")?;
866+
867+
Ok(())
868+
}
869+
870+
#[context("Rolling back composefs")]
871+
pub(crate) async fn composefs_rollback() -> Result<()> {
872+
let host = composefs_deployment_status().await?;
873+
874+
let new_spec = {
875+
let mut new_spec = host.spec.clone();
876+
new_spec.boot_order = new_spec.boot_order.swap();
877+
new_spec
878+
};
879+
880+
// Just to be sure
881+
host.spec.verify_transition(&new_spec)?;
882+
883+
let reverting = new_spec.boot_order == BootOrder::Default;
884+
if reverting {
885+
println!("notice: Reverting queued rollback state");
886+
}
887+
888+
let rollback_status = host
889+
.status
890+
.rollback
891+
.ok_or_else(|| anyhow!("No rollback available"))?;
892+
893+
// TODO: Handle staged deployment
894+
// Ostree will drop any staged deployment on rollback but will keep it if it is the first item
895+
// in the new deployment list
896+
let Some(rollback_composefs_entry) = &rollback_status.composefs else {
897+
anyhow::bail!("Rollback deployment not a composefs deployment")
898+
};
899+
900+
match rollback_composefs_entry.boot_type {
901+
BootType::Bls => rollback_composefs_bls(),
902+
BootType::Uki => rollback_composefs_uki(&host.status.booted.unwrap(), &rollback_status),
903+
}?;
904+
905+
Ok(())
906+
}
907+
743908
/// Implementation of rollback functionality
744909
pub(crate) async fn rollback(sysroot: &Storage) -> Result<()> {
745910
const ROLLBACK_JOURNAL_ID: &str = "26f3b1eb24464d12aa5e7b544a6b5468";

lib/src/install.rs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ use ostree_ext::{
6767
#[cfg(feature = "install-to-disk")]
6868
use rustix::fs::FileTypeExt;
6969
use rustix::fs::MetadataExt as _;
70+
use rustix::path::Arg;
7071
use serde::{Deserialize, Serialize};
7172
use schemars::JsonSchema;
7273

@@ -1696,7 +1697,7 @@ pub fn get_esp_partition(device: &str) -> Result<(String, Option<String>)> {
16961697
Ok((esp.node, esp.uuid))
16971698
}
16981699

1699-
fn get_user_config(uki_id: &str) -> String {
1700+
pub(crate) fn get_user_config(uki_id: &str) -> String {
17001701
let s = format!(
17011702
r#"
17021703
menuentry "Fedora Bootc UKI: ({uki_id})" {{
@@ -1712,15 +1713,27 @@ menuentry "Fedora Bootc UKI: ({uki_id})" {{
17121713
}
17131714

17141715
/// Contains the EFP's filesystem UUID. Used by grub
1715-
const EFI_UUID_FILE: &str = "efiuuid.cfg";
1716+
pub(crate) const EFI_UUID_FILE: &str = "efiuuid.cfg";
1717+
1718+
/// Returns the beginning of the grub2/user.cfg file
1719+
/// where we source a file containing the ESPs filesystem UUID
1720+
pub(crate) fn get_efi_uuid_source() -> String {
1721+
format!(
1722+
r#"
1723+
if [ -f ${{config_directory}}/{EFI_UUID_FILE} ]; then
1724+
source ${{config_directory}}/{EFI_UUID_FILE}
1725+
fi
1726+
"#
1727+
)
1728+
}
17161729

17171730
#[context("Setting up UKI boot")]
17181731
pub(crate) fn setup_composefs_uki_boot(
17191732
setup_type: BootSetupType,
17201733
// TODO: Make this generic
17211734
repo: ComposefsRepository<Sha256HashValue>,
17221735
id: &Sha256HashValue,
1723-
entry: BootEntry<Sha256HashValue>,
1736+
entry: ComposefsBootEntry<Sha256HashValue>,
17241737
) -> Result<()> {
17251738
let (root_path, esp_device) = match setup_type {
17261739
BootSetupType::Setup(root_setup) => {
@@ -1784,13 +1797,7 @@ pub(crate) fn setup_composefs_uki_boot(
17841797

17851798
let is_upgrade = matches!(setup_type, BootSetupType::Upgrade);
17861799

1787-
let efi_uuid_source = format!(
1788-
r#"
1789-
if [ -f ${{config_directory}}/{EFI_UUID_FILE} ]; then
1790-
source ${{config_directory}}/{EFI_UUID_FILE}
1791-
fi
1792-
"#
1793-
);
1800+
let efi_uuid_source = get_efi_uuid_source();
17941801

17951802
let user_cfg_name = if is_upgrade {
17961803
"grub2/user.cfg.staged"
@@ -1800,6 +1807,8 @@ fi
18001807
let user_cfg_path = boot_dir.join(user_cfg_name);
18011808

18021809
// Iterate over all available deployments, and generate a menuentry for each
1810+
//
1811+
// TODO: We might find a staged deployment here
18031812
if is_upgrade {
18041813
let mut usr_cfg = std::fs::OpenOptions::new()
18051814
.write(true)
@@ -1863,7 +1872,7 @@ pub(crate) async fn pull_composefs_repo(
18631872
image: &String,
18641873
) -> Result<(
18651874
ComposefsRepository<Sha256HashValue>,
1866-
Vec<BootEntry<Sha256HashValue>>,
1875+
Vec<ComposefsBootEntry<Sha256HashValue>>,
18671876
Sha256HashValue,
18681877
)> {
18691878
let rootfs_dir = cap_std::fs::Dir::open_ambient_dir("/sysroot", cap_std::ambient_authority())?;

lib/src/spec.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use ostree_ext::{container::OstreeImageReference, oci_spec};
1010
use schemars::JsonSchema;
1111
use serde::{Deserialize, Serialize};
1212

13+
use crate::install::BootType;
1314
use crate::{k8sapitypes, status::Slot};
1415

1516
const API_VERSION: &str = "org.containers.bootc/v1";
@@ -169,6 +170,8 @@ pub struct BootEntryOstree {
169170
pub struct BootEntryComposefs {
170171
/// The erofs verity
171172
pub verity: String,
173+
/// Whether this deployment is to be booted via BLS or UKI
174+
pub boot_type: BootType,
172175
}
173176

174177
/// A bootable entry

0 commit comments

Comments
 (0)