Skip to content

Commit 7dcc0fc

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 6e8a178 commit 7dcc0fc

File tree

7 files changed

+295
-49
lines changed

7 files changed

+295
-49
lines changed

Cargo.lock

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ tini = "1.3.0"
5757
comfy-table = "7.1.1"
5858
thiserror = { workspace = true }
5959
canon-json = { workspace = true }
60+
openat = "0.1.21"
61+
openat-ext = "0.2.3"
6062

6163
[dev-dependencies]
6264
similar-asserts = { workspace = true }

lib/src/cli.rs

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

29-
use crate::deploy::RequiredHostSpec;
29+
use crate::deploy::{composefs_rollback, RequiredHostSpec};
3030
use crate::install::{
3131
pull_composefs_repo, setup_composefs_bls_boot, setup_composefs_uki_boot, write_composefs_state,
3232
BootSetupType, BootType,
@@ -959,8 +959,7 @@ async fn switch_composefs(opts: SwitchOpts) -> Result<()> {
959959
anyhow::bail!("Target image is undefined")
960960
};
961961

962-
let (repo, entries, id) =
963-
pull_composefs_repo(&target_imgref.transport, &target_imgref.image).await?;
962+
let (repo, entries, id) = pull_composefs_repo(&"docker".into(), &target_imgref.image).await?;
964963

965964
let Some(entry) = entries.into_iter().next() else {
966965
anyhow::bail!("No boot entries!");
@@ -1053,8 +1052,12 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
10531052
/// Implementation of the `bootc rollback` CLI command.
10541053
#[context("Rollback")]
10551054
async fn rollback(opts: RollbackOpts) -> Result<()> {
1056-
let sysroot = &get_storage().await?;
1057-
crate::deploy::rollback(sysroot).await?;
1055+
if composefs_booted()? {
1056+
composefs_rollback().await?
1057+
} else {
1058+
let sysroot = &get_storage().await?;
1059+
crate::deploy::rollback(sysroot).await?;
1060+
};
10581061

10591062
if opts.apply {
10601063
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

@@ -747,6 +753,165 @@ pub(crate) async fn stage(
747753
Ok(())
748754
}
749755

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

lib/src/install.rs

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

@@ -1694,7 +1695,7 @@ pub fn get_esp_partition(device: &str) -> Result<(String, Option<String>)> {
16941695
Ok((esp.node, esp.uuid))
16951696
}
16961697

1697-
fn get_user_config(uki_id: &str) -> String {
1698+
pub(crate) fn get_user_config(uki_id: &str) -> String {
16981699
let s = format!(
16991700
r#"
17001701
menuentry "Fedora Bootc UKI: ({uki_id})" {{
@@ -1710,15 +1711,27 @@ menuentry "Fedora Bootc UKI: ({uki_id})" {{
17101711
}
17111712

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

17151728
#[context("Setting up UKI boot")]
17161729
pub(crate) fn setup_composefs_uki_boot(
17171730
setup_type: BootSetupType,
17181731
// TODO: Make this generic
17191732
repo: ComposefsRepository<Sha256HashValue>,
17201733
id: &Sha256HashValue,
1721-
entry: BootEntry<Sha256HashValue>,
1734+
entry: ComposefsBootEntry<Sha256HashValue>,
17221735
) -> Result<()> {
17231736
let (root_path, esp_device) = match setup_type {
17241737
BootSetupType::Setup(root_setup) => {
@@ -1782,13 +1795,7 @@ pub(crate) fn setup_composefs_uki_boot(
17821795

17831796
let is_upgrade = matches!(setup_type, BootSetupType::Upgrade);
17841797

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

17931800
let user_cfg_name = if is_upgrade {
17941801
"grub2/user.cfg.staged"
@@ -1798,6 +1805,8 @@ fi
17981805
let user_cfg_path = boot_dir.join(user_cfg_name);
17991806

18001807
// Iterate over all available deployments, and generate a menuentry for each
1808+
//
1809+
// TODO: We might find a staged deployment here
18011810
if is_upgrade {
18021811
let mut usr_cfg = std::fs::OpenOptions::new()
18031812
.write(true)
@@ -1861,7 +1870,7 @@ pub(crate) async fn pull_composefs_repo(
18611870
image: &String,
18621871
) -> Result<(
18631872
ComposefsRepository<Sha256HashValue>,
1864-
Vec<BootEntry<Sha256HashValue>>,
1873+
Vec<ComposefsBootEntry<Sha256HashValue>>,
18651874
Sha256HashValue,
18661875
)> {
18671876
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";
@@ -171,6 +172,8 @@ pub struct BootEntryOstree {
171172
pub struct BootEntryComposefs {
172173
/// The erofs verity
173174
pub verity: String,
175+
/// Whether this deployment is to be booted via BLS or UKI
176+
pub boot_type: BootType,
174177
}
175178

176179
/// A bootable entry

0 commit comments

Comments
 (0)