refactor boot selection algorithm
This commit is contained in:
parent
8f8cbab72f
commit
d244c07483
@ -1,140 +0,0 @@
|
||||
|
||||
use libcitadel::{Partition,Result,ImageHeader};
|
||||
|
||||
pub struct BootSelection {
|
||||
partitions: Vec<Partition>,
|
||||
}
|
||||
|
||||
impl BootSelection {
|
||||
pub fn choose_install_partition() -> Result<Partition> {
|
||||
let bs = BootSelection::load_partitions()?;
|
||||
match bs._choose_install_partition() {
|
||||
Some(p) => Ok(p.clone()),
|
||||
None => bail!("no partition found for installation"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn choose_boot_partition() -> Result<Partition> {
|
||||
let bs = BootSelection::load_partitions()?;
|
||||
match bs._choose_boot_partition() {
|
||||
Some(p) => Ok(p.clone()),
|
||||
None => bail!("no partition found to boot from"),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_partitions() -> Result<BootSelection> {
|
||||
let partitions = Partition::rootfs_partitions()
|
||||
.map_err(|e| format_err!("Could not load rootfs partition info: {}", e))?;
|
||||
|
||||
Ok(BootSelection {
|
||||
partitions
|
||||
})
|
||||
}
|
||||
|
||||
fn _choose_install_partition(&self) -> Option<&Partition> {
|
||||
self.choose(|p| {
|
||||
// first pass, if there is a partition which is not mounted and
|
||||
// not initialized use that one
|
||||
!p.is_mounted() && !p.is_initialized()
|
||||
}).or_else(|| self.choose(|p| {
|
||||
// second pass, just find one that's not mounted
|
||||
!p.is_mounted()
|
||||
}))
|
||||
}
|
||||
|
||||
fn choose<F>(&self, pred: F) -> Option<&Partition>
|
||||
where F: Sized + Fn(&&Partition) -> bool
|
||||
{
|
||||
self.partitions.iter().find(pred)
|
||||
}
|
||||
|
||||
/// Find the best rootfs partition to boot from
|
||||
fn _choose_boot_partition(&self) -> Option<&Partition> {
|
||||
let mut best: Option<&Partition> = None;
|
||||
|
||||
for p in &self.partitions {
|
||||
if is_better(&best, p) {
|
||||
best = Some(p);
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
|
||||
/// Perform checks for error states at boot time.
|
||||
pub fn scan_boot_partitions(&mut self) -> Result<()> {
|
||||
for mut p in &mut self.partitions {
|
||||
if let Err(e) = boot_scan_partition(&mut p) {
|
||||
warn!("error in bootscan of partition {}: {}", p.path().display(), e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Called at boot to perform various checks and possibly
|
||||
/// update the status field to an error state.
|
||||
///
|
||||
/// Mark `STATUS_TRY_BOOT` partition as `STATUS_FAILED`.
|
||||
///
|
||||
/// If metainfo cannot be parsed, mark as `STATUS_BAD_META`.
|
||||
///
|
||||
/// Verify metainfo signature and mark `STATUS_BAD_SIG` if
|
||||
/// signature verification fails.
|
||||
///
|
||||
fn boot_scan_partition(p: &mut Partition) -> Result<()> {
|
||||
if !p.is_initialized() {
|
||||
return Ok(())
|
||||
}
|
||||
if p.header().status() == ImageHeader::STATUS_TRY_BOOT {
|
||||
warn!("Partition {} has STATUS_TRY_BOOT, assuming it failed boot attempt and marking STATUS_FAILED", p.path().display());
|
||||
p.write_status(ImageHeader::STATUS_FAILED)?;
|
||||
}
|
||||
p.header().verify_signature()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_better<'a>(current_best: &Option<&'a Partition>, other: &'a Partition) -> bool {
|
||||
|
||||
if !other.is_initialized() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only consider partitions in state NEW or state GOOD
|
||||
if !other.is_good() && !other.is_new() {
|
||||
return false;
|
||||
}
|
||||
// If metainfo is broken, then no, it's not better
|
||||
//if !other.metainfo().is_ok() {
|
||||
// return false;
|
||||
//}
|
||||
|
||||
let best = match *current_best {
|
||||
Some(p) => p,
|
||||
// No current 'best', so 'other' is better, whatever it is.
|
||||
None => return true,
|
||||
};
|
||||
|
||||
// First parition with PREFER flag trumps everything else
|
||||
if best.is_preferred() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let best_version = best.metainfo().version();
|
||||
let other_version = other.metainfo().version();
|
||||
|
||||
if best_version > other_version {
|
||||
return false;
|
||||
}
|
||||
|
||||
if other_version > best_version {
|
||||
return true;
|
||||
}
|
||||
|
||||
// choose NEW over GOOD if versions are the same
|
||||
if other.is_new() && best.is_good() {
|
||||
return true;
|
||||
}
|
||||
// ... but if all things otherwise match, return first match
|
||||
false
|
||||
}
|
@ -10,11 +10,7 @@ use std::fs;
|
||||
use libcitadel::{Result,CommandLine,set_verbose,format_error,ResourceImage,util};
|
||||
|
||||
|
||||
mod boot_select;
|
||||
mod rootfs;
|
||||
pub use boot_select::BootSelection;
|
||||
use rootfs::Rootfs;
|
||||
|
||||
|
||||
/// mount command supports 4 subcommands
|
||||
///
|
||||
@ -27,7 +23,7 @@ use rootfs::Rootfs;
|
||||
///
|
||||
/// 'kernel' mounts a resource bundle containing kernel modules
|
||||
/// 'extra' mounts a resource bundle containing extra files
|
||||
///
|
||||
/// 'overlay' mounts a tmpfs overlay over rootfs filesystem only if citadel.overlay is set
|
||||
///
|
||||
|
||||
fn main() {
|
||||
@ -54,8 +50,7 @@ fn main() {
|
||||
|
||||
fn mount_rootfs() -> Result<()> {
|
||||
info!("citadel-mount rootfs");
|
||||
let rootfs = Rootfs::new();
|
||||
rootfs.setup()
|
||||
rootfs::setup_rootfs()
|
||||
}
|
||||
|
||||
fn mount_kernel() -> Result<()> {
|
||||
|
@ -1,93 +1,174 @@
|
||||
use std::process::Command;
|
||||
|
||||
use libcitadel::{BlockDev,CommandLine,Partition,Result,verity};
|
||||
use libcitadel::{BlockDev,CommandLine,ImageHeader,Partition,Result,verity};
|
||||
use std::path::Path;
|
||||
use std::process::Stdio;
|
||||
use BootSelection;
|
||||
use ResourceImage;
|
||||
|
||||
pub struct Rootfs {
|
||||
}
|
||||
|
||||
impl Rootfs {
|
||||
pub fn new() -> Rootfs {
|
||||
Rootfs {}
|
||||
}
|
||||
|
||||
pub fn setup(&self) -> Result<()> {
|
||||
if CommandLine::install_mode() || CommandLine::live_mode() {
|
||||
self.setup_rootfs_resource()
|
||||
} else {
|
||||
let partition = BootSelection::choose_boot_partition()?;
|
||||
self.setup_partition(partition)
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_partition(&self, partition: Partition) -> Result<()> {
|
||||
if CommandLine::noverity() {
|
||||
self.setup_partition_unverified(&partition)
|
||||
} else {
|
||||
self.setup_partition_verified(&partition)
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_rootfs_resource(&self) -> Result<()> {
|
||||
info!("Searching for rootfs resource image");
|
||||
|
||||
let img = ResourceImage::find_rootfs()?;
|
||||
|
||||
if CommandLine::noverity() {
|
||||
self.setup_resource_unverified(&img)
|
||||
} else {
|
||||
self.setup_resource_verified(&img)
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_resource_unverified(&self, img: &ResourceImage) -> Result<()> {
|
||||
if img.is_compressed() {
|
||||
img.decompress()?;
|
||||
}
|
||||
let loopdev = img.create_loopdev()?;
|
||||
info!("Loop device created: {}", loopdev.display());
|
||||
self.setup_linear_mapping(&loopdev)
|
||||
}
|
||||
|
||||
fn setup_resource_verified(&self, img: &ResourceImage) -> Result<()> {
|
||||
let _ = img.setup_verity_device()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_partition_unverified(&self, partition: &Partition) -> Result<()> {
|
||||
info!("Creating /dev/mapper/rootfs device with linear device mapping of partition (no verity)");
|
||||
self.setup_linear_mapping(partition.path())
|
||||
}
|
||||
|
||||
fn setup_partition_verified(&self, partition: &Partition) -> Result<()> {
|
||||
info!("Creating /dev/mapper/rootfs dm-verity device");
|
||||
if !CommandLine::nosignatures() {
|
||||
partition.header().verify_signature()?;
|
||||
info!("Image signature is valid for channel {}", partition.metainfo().channel());
|
||||
}
|
||||
verity::setup_partition_device(partition)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_linear_mapping(&self, blockdev: &Path) -> Result<()> {
|
||||
let dev = BlockDev::open_ro(blockdev)?;
|
||||
let table = format!("0 {} linear {} 0", dev.nsectors()?, blockdev.display());
|
||||
|
||||
info!("/usr/sbin/dmsetup create rootfs --table '{}'", table);
|
||||
|
||||
let ok = Command::new("/usr/sbin/dmsetup")
|
||||
.args(&["create", "rootfs", "--table", &table])
|
||||
.stderr(Stdio::inherit())
|
||||
.status()
|
||||
.expect("unable to execute /usr/sbin/dmsetup")
|
||||
.success();
|
||||
|
||||
if !ok {
|
||||
bail!("Failed to set up linear identity mapping with /usr/sbin/dmsetup");
|
||||
}
|
||||
Ok(())
|
||||
pub fn setup_rootfs() -> Result<()> {
|
||||
if CommandLine::install_mode() || CommandLine::live_mode() {
|
||||
setup_rootfs_resource()
|
||||
} else {
|
||||
let p = choose_boot_partiton(true)?;
|
||||
setup_partition(p)
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_partition(mut p: Partition) -> Result<()> {
|
||||
if CommandLine::noverity() {
|
||||
setup_partition_unverified(&p)
|
||||
} else {
|
||||
setup_partition_verified(&mut p)
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_rootfs_resource() -> Result<()> {
|
||||
info!("Searching for rootfs resource image");
|
||||
|
||||
let img = ResourceImage::find_rootfs()?;
|
||||
|
||||
if CommandLine::noverity() {
|
||||
setup_resource_unverified(&img)
|
||||
} else {
|
||||
setup_resource_verified(&img)
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_resource_unverified(img: &ResourceImage) -> Result<()> {
|
||||
if img.is_compressed() {
|
||||
img.decompress()?;
|
||||
}
|
||||
let loopdev = img.create_loopdev()?;
|
||||
info!("Loop device created: {}", loopdev.display());
|
||||
setup_linear_mapping(&loopdev)
|
||||
}
|
||||
|
||||
fn setup_resource_verified(img: &ResourceImage) -> Result<()> {
|
||||
let _ = img.setup_verity_device()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_partition_unverified(p: &Partition) -> Result<()> {
|
||||
info!("Creating /dev/mapper/rootfs device with linear device mapping of partition (no verity)");
|
||||
setup_linear_mapping(p.path())
|
||||
}
|
||||
|
||||
fn setup_partition_verified(p: &mut Partition) -> Result<()> {
|
||||
info!("Creating /dev/mapper/rootfs dm-verity device");
|
||||
if !CommandLine::nosignatures() {
|
||||
if !p.has_public_key() {
|
||||
bail!("No public key available for channel {}", p.metainfo().channel())
|
||||
}
|
||||
if !p.is_signature_valid() {
|
||||
p.write_status(ImageHeader::STATUS_BAD_SIG)?;
|
||||
bail!("Signature verification failed on partition");
|
||||
}
|
||||
info!("Image signature is valid for channel {}", p.metainfo().channel());
|
||||
}
|
||||
verity::setup_partition_device(p)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_linear_mapping(blockdev: &Path) -> Result<()> {
|
||||
let dev = BlockDev::open_ro(blockdev)?;
|
||||
let table = format!("0 {} linear {} 0", dev.nsectors()?, blockdev.display());
|
||||
|
||||
info!("/usr/sbin/dmsetup create rootfs --table '{}'", table);
|
||||
|
||||
let ok = Command::new("/usr/sbin/dmsetup")
|
||||
.args(&["create", "rootfs", "--table", &table])
|
||||
.stderr(Stdio::inherit())
|
||||
.status()
|
||||
.expect("unable to execute /usr/sbin/dmsetup")
|
||||
.success();
|
||||
|
||||
if !ok {
|
||||
bail!("Failed to set up linear identity mapping with /usr/sbin/dmsetup");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn choose_boot_partiton(scan: bool) -> Result<Partition> {
|
||||
let mut partitions = Partition::rootfs_partitions()?;
|
||||
|
||||
if scan {
|
||||
for p in &mut partitions {
|
||||
p.boot_scan()?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut best = None;
|
||||
for p in partitions {
|
||||
best = compare_boot_partitions(best, p);
|
||||
}
|
||||
best.ok_or(format_err!("No partition found to boot from"))
|
||||
}
|
||||
|
||||
fn compare_boot_partitions(a: Option<Partition>, b: Partition) -> Option<Partition> {
|
||||
if !is_bootable(&b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
// b is bootable, so if a is None, then just return b
|
||||
let a = match a {
|
||||
Some(partition) => partition,
|
||||
None => return Some(b),
|
||||
};
|
||||
|
||||
// First partition with FLAG_PREFER_BOOT trumps everything
|
||||
if a.is_preferred() {
|
||||
return Some(a);
|
||||
}
|
||||
|
||||
if b.is_preferred() {
|
||||
return Some(b);
|
||||
}
|
||||
|
||||
// Compare versions and channels
|
||||
let a_v = a.metainfo().version();
|
||||
let b_v = b.metainfo().version();
|
||||
|
||||
// Compare versions only if channels match
|
||||
if a.metainfo().channel() == b.metainfo().channel() {
|
||||
if a_v > b_v {
|
||||
return Some(a);
|
||||
} else if b_v > a_v {
|
||||
return Some(b);
|
||||
}
|
||||
}
|
||||
|
||||
// choose NEW over GOOD if versions are the same or
|
||||
// if versions cannot be compared because channels differ
|
||||
if b.is_new() && a.is_good() {
|
||||
return Some(b);
|
||||
}
|
||||
|
||||
Some(a)
|
||||
}
|
||||
|
||||
fn is_bootable(p: &Partition) -> bool {
|
||||
if !p.is_initialized() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// signatures enabled so not bootable without pubkey
|
||||
if signatures_enabled() && !p.has_public_key() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if p.is_new() || p.is_good() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If signatures are disabled then don't disqualify an
|
||||
// image which failed a prior signature verification
|
||||
if !signatures_enabled() && p.is_sig_failed() {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn signatures_enabled() -> bool {
|
||||
!(CommandLine::nosignatures() || CommandLine::noverity())
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user