Skip to content

Commit af1e0df

Browse files
composefs/status: Check if deployment is soft rebootable
Signed-off-by: Pragyan Poudyal <[email protected]>
1 parent 72f1f27 commit af1e0df

File tree

4 files changed

+222
-23
lines changed

4 files changed

+222
-23
lines changed

crates/lib/src/bootc_composefs/status.rs

Lines changed: 197 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ use serde::{Deserialize, Serialize};
77

88
use crate::{
99
bootc_composefs::{boot::BootType, repo::get_imgref},
10-
composefs_consts::{COMPOSEFS_CMDLINE, ORIGIN_KEY_BOOT_DIGEST, TYPE1_ENT_PATH, USER_CFG},
10+
composefs_consts::{
11+
COMPOSEFS_CMDLINE, ORIGIN_KEY_BOOT_DIGEST, TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED, USER_CFG,
12+
},
1113
install::EFI_LOADER_INFO,
1214
parsers::{
1315
bls_config::{parse_bls_config, BLSConfig, BLSConfigType},
@@ -100,14 +102,43 @@ pub(crate) fn get_sorted_grub_uki_boot_entries<'a>(
100102
parse_grub_menuentry_file(str)
101103
}
102104

103-
#[context("Getting sorted Type1 boot entries")]
104105
pub(crate) fn get_sorted_type1_boot_entries(
105106
boot_dir: &Dir,
106107
ascending: bool,
108+
) -> Result<Vec<BLSConfig>> {
109+
get_sorted_type1_boot_entries_helper(boot_dir, ascending, false)
110+
}
111+
112+
pub(crate) fn get_sorted_staged_type1_boot_entries(
113+
boot_dir: &Dir,
114+
ascending: bool,
115+
) -> Result<Vec<BLSConfig>> {
116+
get_sorted_type1_boot_entries_helper(boot_dir, ascending, true)
117+
}
118+
119+
#[context("Getting sorted Type1 boot entries")]
120+
fn get_sorted_type1_boot_entries_helper(
121+
boot_dir: &Dir,
122+
ascending: bool,
123+
get_staged_entries: bool,
107124
) -> Result<Vec<BLSConfig>> {
108125
let mut all_configs = vec![];
109126

110-
for entry in boot_dir.read_dir(TYPE1_ENT_PATH)? {
127+
let dir = match get_staged_entries {
128+
true => {
129+
let dir = boot_dir.open_dir_optional(TYPE1_ENT_PATH_STAGED)?;
130+
131+
let Some(dir) = dir else {
132+
return Ok(all_configs);
133+
};
134+
135+
dir.read_dir(".")?
136+
}
137+
138+
false => boot_dir.read_dir(TYPE1_ENT_PATH)?,
139+
};
140+
141+
for entry in dir {
111142
let entry = entry?;
112143

113144
let file_name = entry.file_name();
@@ -302,12 +333,140 @@ pub(crate) async fn get_composefs_status(
302333
composefs_deployment_status_from(&storage, booted_cfs.cmdline).await
303334
}
304335

336+
fn set_soft_reboot_capable_bls(
337+
storage: &Storage,
338+
host: &mut Host,
339+
bls_entries: &Vec<BLSConfig>,
340+
cmdline: &ComposefsCmdline,
341+
) -> Result<()> {
342+
let booted = host.require_composefs_booted()?;
343+
344+
match booted.boot_type {
345+
BootType::Bls => {
346+
set_reboot_capable_type1_deployments(storage, cmdline, host, bls_entries)?;
347+
}
348+
349+
BootType::Uki => match booted.bootloader {
350+
Bootloader::Grub => todo!(),
351+
Bootloader::Systemd => todo!(),
352+
},
353+
};
354+
355+
Ok(())
356+
}
357+
358+
fn find_bls_entry<'a>(
359+
verity: &str,
360+
bls_entries: &'a Vec<BLSConfig>,
361+
) -> Result<Option<&'a BLSConfig>> {
362+
for ent in bls_entries {
363+
if ent.get_verity()? == *verity {
364+
return Ok(Some(ent));
365+
}
366+
}
367+
368+
Ok(None)
369+
}
370+
371+
/// Compares cmdline `first` and `second` skipping `composefs=`
372+
fn compare_cmdline_skip_cfs(first: &Cmdline<'_>, second: &Cmdline<'_>) -> bool {
373+
for param in first {
374+
if param.key() == COMPOSEFS_CMDLINE.into() {
375+
continue;
376+
}
377+
378+
let second_param = second.iter().find(|b| *b == param);
379+
380+
let Some(found_param) = second_param else {
381+
return false;
382+
};
383+
384+
if found_param.value() != param.value() {
385+
return false;
386+
}
387+
}
388+
389+
return true;
390+
}
391+
392+
fn set_soft_reboot_capable_type1(
393+
deployment: &mut BootEntry,
394+
bls_entries: &Vec<BLSConfig>,
395+
booted_bls_entry: &BLSConfig,
396+
booted_boot_digest: &String,
397+
) -> Result<()> {
398+
let deployment_cfs = deployment.require_composefs()?;
399+
400+
// TODO: Unwrap
401+
if deployment_cfs.boot_digest.as_ref().unwrap() != booted_boot_digest {
402+
deployment.soft_reboot_capable = false;
403+
return Ok(());
404+
}
405+
406+
let entry = find_bls_entry(&deployment_cfs.verity, bls_entries)?
407+
.ok_or_else(|| anyhow::anyhow!("Entry not found"))?;
408+
409+
let opts = entry.get_cmdline()?;
410+
let booted_cmdline_opts = booted_bls_entry.get_cmdline()?;
411+
412+
if opts.len() != booted_cmdline_opts.len() {
413+
tracing::debug!("Soft reboot not allowed due to differing cmdline");
414+
deployment.soft_reboot_capable = false;
415+
return Ok(());
416+
}
417+
418+
deployment.soft_reboot_capable = compare_cmdline_skip_cfs(opts, booted_cmdline_opts)
419+
&& compare_cmdline_skip_cfs(booted_cmdline_opts, opts);
420+
421+
return Ok(());
422+
}
423+
424+
fn set_reboot_capable_type1_deployments(
425+
storage: &Storage,
426+
cmdline: &ComposefsCmdline,
427+
host: &mut Host,
428+
bls_entries: &Vec<BLSConfig>,
429+
) -> Result<()> {
430+
let booted = host
431+
.status
432+
.booted
433+
.as_ref()
434+
.ok_or_else(|| anyhow::anyhow!("Failed to find booted entry"))?;
435+
436+
let booted_boot_digest = booted.composefs_boot_digest()?;
437+
438+
let booted_bls_entry = find_bls_entry(&*cmdline.digest, bls_entries)?
439+
.ok_or_else(|| anyhow::anyhow!("Booted bls entry not found"))?;
440+
441+
if let Some(staged) = host.status.staged.as_mut() {
442+
let staged_entries =
443+
get_sorted_staged_type1_boot_entries(storage.require_boot_dir()?, true)?;
444+
445+
set_soft_reboot_capable_type1(
446+
staged,
447+
&staged_entries,
448+
booted_bls_entry,
449+
booted_boot_digest,
450+
)?;
451+
}
452+
453+
if let Some(rollback) = &mut host.status.rollback {
454+
set_soft_reboot_capable_type1(rollback, bls_entries, booted_bls_entry, booted_boot_digest)?;
455+
}
456+
457+
for depl in &mut host.status.other_deployments {
458+
set_soft_reboot_capable_type1(depl, bls_entries, booted_bls_entry, booted_boot_digest)?;
459+
}
460+
461+
Ok(())
462+
}
463+
305464
#[context("Getting composefs deployment status")]
306465
pub(crate) async fn composefs_deployment_status_from(
307466
storage: &Storage,
308467
cmdline: &ComposefsCmdline,
309468
) -> Result<Host> {
310-
let composefs_digest = &cmdline.digest;
469+
let booted_composefs_digest = &cmdline.digest;
311470

312471
let boot_dir = storage.require_boot_dir()?;
313472

@@ -373,7 +532,7 @@ pub(crate) async fn composefs_deployment_status_from(
373532
}
374533
};
375534

376-
if depl.file_name() == composefs_digest.as_ref() {
535+
if depl.file_name() == booted_composefs_digest.as_ref() {
377536
host.spec.image = boot_entry.image.as_ref().map(|x| x.image.clone());
378537
host.status.booted = Some(boot_entry);
379538
continue;
@@ -394,60 +553,72 @@ pub(crate) async fn composefs_deployment_status_from(
394553
anyhow::bail!("Could not determine boot type");
395554
};
396555

397-
let booted = host.require_composefs_booted()?;
556+
let booted_cfs = host.require_composefs_booted()?;
398557

399-
let is_rollback_queued = match booted.bootloader {
558+
let (is_rollback_queued, sorted_bls_config) = match booted_cfs.bootloader {
400559
Bootloader::Grub => match boot_type {
401560
BootType::Bls => {
402-
let bls_config = get_sorted_type1_boot_entries(boot_dir, false)?;
403-
let bls_config = bls_config
561+
let bls_configs = get_sorted_type1_boot_entries(boot_dir, false)?;
562+
let bls_config = bls_configs
404563
.first()
405-
.ok_or(anyhow::anyhow!("First boot entry not found"))?;
564+
.ok_or_else(|| anyhow::anyhow!("First boot entry not found"))?;
406565

407566
match &bls_config.cfg_type {
408-
BLSConfigType::NonEFI { options, .. } => !options
409-
.as_ref()
410-
.ok_or(anyhow::anyhow!("options key not found in bls config"))?
411-
.contains(composefs_digest.as_ref()),
567+
BLSConfigType::NonEFI { options, .. } => {
568+
let is_rollback_queued = !options
569+
.as_ref()
570+
.ok_or_else(|| anyhow::anyhow!("options key not found in bls config"))?
571+
.contains(booted_composefs_digest.as_ref());
572+
573+
(is_rollback_queued, Some(bls_configs))
574+
}
412575

413576
BLSConfigType::EFI { .. } => {
414577
anyhow::bail!("Found 'efi' field in Type1 boot entry")
415578
}
579+
416580
BLSConfigType::Unknown => anyhow::bail!("Unknown BLS Config Type"),
417581
}
418582
}
419583

420584
BootType::Uki => {
421585
let mut s = String::new();
586+
let menuentries = get_sorted_grub_uki_boot_entries(boot_dir, &mut s)?;
422587

423-
!get_sorted_grub_uki_boot_entries(boot_dir, &mut s)?
588+
let is_rollback_queued = !menuentries
424589
.first()
425590
.ok_or(anyhow::anyhow!("First boot entry not found"))?
426591
.body
427592
.chainloader
428-
.contains(composefs_digest.as_ref())
593+
.contains(booted_composefs_digest.as_ref());
594+
595+
(is_rollback_queued, None)
429596
}
430597
},
431598

432599
// We will have BLS stuff and the UKI stuff in the same DIR
433600
Bootloader::Systemd => {
434-
let bls_config = get_sorted_type1_boot_entries(boot_dir, false)?;
435-
let bls_config = bls_config
601+
let bls_configs = get_sorted_type1_boot_entries(boot_dir, true)?;
602+
let bls_config = bls_configs
436603
.first()
437604
.ok_or(anyhow::anyhow!("First boot entry not found"))?;
438605

439-
match &bls_config.cfg_type {
606+
let is_rollback_queued = match &bls_config.cfg_type {
440607
// For UKI boot
441-
BLSConfigType::EFI { efi } => efi.as_str().contains(composefs_digest.as_ref()),
608+
BLSConfigType::EFI { efi } => {
609+
efi.as_str().contains(booted_composefs_digest.as_ref())
610+
}
442611

443612
// For boot entry Type1
444613
BLSConfigType::NonEFI { options, .. } => !options
445614
.as_ref()
446615
.ok_or(anyhow::anyhow!("options key not found in bls config"))?
447-
.contains(composefs_digest.as_ref()),
616+
.contains(booted_composefs_digest.as_ref()),
448617

449618
BLSConfigType::Unknown => anyhow::bail!("Unknown BLS Config Type"),
450-
}
619+
};
620+
621+
(is_rollback_queued, Some(bls_configs))
451622
}
452623
};
453624

@@ -457,6 +628,10 @@ pub(crate) async fn composefs_deployment_status_from(
457628
host.spec.boot_order = BootOrder::Rollback
458629
};
459630

631+
if let Some(bls_configs) = sorted_bls_config {
632+
set_soft_reboot_capable_bls(storage, &mut host, &bls_configs, cmdline)?;
633+
}
634+
460635
Ok(host)
461636
}
462637

crates/lib/src/cli.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ use std::process::Command;
1010

1111
use anyhow::{anyhow, ensure, Context, Result};
1212
use camino::{Utf8Path, Utf8PathBuf};
13-
use cap_std_ext::cap_std;
1413
use cap_std_ext::cap_std::fs::Dir;
14+
use cap_std_ext::cap_std;
1515
use clap::Parser;
1616
use clap::ValueEnum;
1717
use composefs::dumpfile;

crates/lib/src/parsers/bls_config.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,23 @@ impl BLSConfig {
203203
BLSConfigType::Unknown => anyhow::bail!("Unknown config type"),
204204
}
205205
}
206+
207+
/// Gets the `options` field from the config
208+
/// Returns an error if the field doesn't exist
209+
/// or if the config is of type `EFI`
210+
pub(crate) fn get_cmdline(&self) -> Result<&Cmdline<'_>> {
211+
match &self.cfg_type {
212+
BLSConfigType::NonEFI { options, .. } => {
213+
let options = options
214+
.as_ref()
215+
.ok_or_else(|| anyhow::anyhow!("No cmdline found for config"))?;
216+
217+
Ok(options)
218+
}
219+
220+
_ => anyhow::bail!("No cmdline found for config"),
221+
}
222+
}
206223
}
207224

208225
pub(crate) fn parse_bls_config(input: &str) -> Result<BLSConfig> {

crates/lib/src/status.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,13 @@ impl BootEntry {
309309
"BootEntry is not a composefs native boot entry"
310310
))
311311
}
312+
313+
pub(crate) fn composefs_boot_digest(&self) -> Result<&String> {
314+
self.require_composefs()?
315+
.boot_digest
316+
.as_ref()
317+
.ok_or_else(|| anyhow::anyhow!("Could not find boot digest for deployment"))
318+
}
312319
}
313320

314321
/// A variant of [`get_status`] that requires a booted deployment.

0 commit comments

Comments
 (0)