refactor boot selection algorithm

This commit is contained in:
Bruce Leidl 2019-01-17 09:10:28 -05:00
parent 8f8cbab72f
commit d244c07483
3 changed files with 168 additions and 232 deletions

View File

@ -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
}

View File

@ -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<()> {

View File

@ -1,78 +1,75 @@
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 {
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)
}
}
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<()> {
fn setup_partition(mut p: Partition) -> Result<()> {
if CommandLine::noverity() {
self.setup_partition_unverified(&partition)
setup_partition_unverified(&p)
} else {
self.setup_partition_verified(&partition)
}
setup_partition_verified(&mut p)
}
}
fn setup_rootfs_resource(&self) -> Result<()> {
fn setup_rootfs_resource() -> Result<()> {
info!("Searching for rootfs resource image");
let img = ResourceImage::find_rootfs()?;
if CommandLine::noverity() {
self.setup_resource_unverified(&img)
setup_resource_unverified(&img)
} else {
self.setup_resource_verified(&img)
}
setup_resource_verified(&img)
}
}
fn setup_resource_unverified(&self, img: &ResourceImage) -> Result<()> {
fn setup_resource_unverified(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)
}
setup_linear_mapping(&loopdev)
}
fn setup_resource_verified(&self, img: &ResourceImage) -> Result<()> {
fn setup_resource_verified(img: &ResourceImage) -> Result<()> {
let _ = img.setup_verity_device()?;
Ok(())
}
}
fn setup_partition_unverified(&self, partition: &Partition) -> Result<()> {
fn setup_partition_unverified(p: &Partition) -> Result<()> {
info!("Creating /dev/mapper/rootfs device with linear device mapping of partition (no verity)");
self.setup_linear_mapping(partition.path())
}
setup_linear_mapping(p.path())
}
fn setup_partition_verified(&self, partition: &Partition) -> Result<()> {
fn setup_partition_verified(p: &mut 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());
if !p.has_public_key() {
bail!("No public key available for channel {}", p.metainfo().channel())
}
verity::setup_partition_device(partition)?;
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(&self, blockdev: &Path) -> Result<()> {
fn setup_linear_mapping(blockdev: &Path) -> Result<()> {
let dev = BlockDev::open_ro(blockdev)?;
let table = format!("0 {} linear {} 0", dev.nsectors()?, blockdev.display());
@ -89,5 +86,89 @@ impl Rootfs {
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())
}