Initial commit of update utility to simplify image upgrades

This commit is contained in:
Bruce Leidl 2019-08-21 00:25:18 -04:00
parent 77f19777a8
commit ba29516212
3 changed files with 821 additions and 0 deletions

View File

@ -15,6 +15,7 @@ mod install;
mod mkimage;
mod realmfs;
mod sync;
mod update;
fn main() {
let exe = match env::current_exe() {
@ -34,6 +35,8 @@ fn main() {
image::main(args);
} else if exe == Path::new("/usr/bin/citadel-realmfs") {
realmfs::main(args);
} else if exe == Path::new("/usr/bin/citadel-update") {
update::main(args);
} else if exe == Path::new("/usr/libexec/citadel-desktop-sync") {
sync::main(args);
} else if exe == Path::new("/usr/libexec/citadel-run") {
@ -54,6 +57,7 @@ fn dispatch_command(args: Vec<String>) {
"install" => install::main(rebuild_args("citadel-install", args)),
"image" => image::main(rebuild_args("citadel-image", args)),
"realmfs" => realmfs::main(rebuild_args("citadel-realmfs", args)),
"update" => update::main(rebuild_args("citadel-update", args)),
"mkimage" => mkimage::main(rebuild_args("citadel-mkimage", args)),
"sync" => sync::main(rebuild_args("citadel-desktop-sync", args)),
"run" => do_citadel_run(rebuild_args("citadel-run", args)),

View File

@ -0,0 +1,479 @@
use std::fs;
use std::fmt::{self,Write};
use std::path::{Path,PathBuf};
use libcitadel::{Result,util};
const DEFAULT_MAX_ENTRIES: usize = 3;
const DEFAULT_BOOT_COUNT: u32 = 3;
const DEFAULT_KERNEL_CMDLINE: &str = "root=/dev/mapper/rootfs add_efi_memmap intel_iommu=off cryptomgr.notests rcupdate.rcu_expedited=1 rcu_nocbs=0-64 tsc=reliable no_timer_check noreplace-smp i915.fastboot=1 quiet splash";
pub struct KernelInstaller {
max_entries: usize,
new_kernel: KernelBzImage,
all_entries: BootEntries,
boot_entries: BootEntries,
}
impl KernelInstaller {
pub fn install_kernel(new_kernel: &Path, version: &str) -> Result<()> {
let mut installer = Self::new(new_kernel, version)?;
if installer.is_already_installed() {
bail!("identical kernel is is already installed");
}
installer.install()?;
Ok(())
}
pub fn new(new_kernel: &Path, version: &str) -> Result<KernelInstaller> {
let new_kernel = KernelBzImage::from_path_and_version(new_kernel.to_path_buf(), version)?;
let all_entries = BootEntries::load()?;
let boot_entries = all_entries.find_by_name("boot");
Ok(KernelInstaller {
max_entries: DEFAULT_MAX_ENTRIES,
new_kernel,
all_entries,
boot_entries,
})
}
pub fn is_already_installed(&self) -> bool {
self.all_entries.0.iter()
.flat_map(|e| e.bzimage.as_ref())
.any(|k| k.shasum == self.new_kernel.shasum)
}
pub fn install(&mut self) -> Result<PathBuf> {
let install_path = self.install_kernel_path()?;
info!("Copying kernel bzImage to {}", install_path.display());
fs::copy(&self.new_kernel.path, &install_path)?;
self.boot_entries.rotate()?;
let options = self.generate_options_line();
let entry = BootEntry::create_for_kernel("boot", self.new_kernel.clone(), options, Some(DEFAULT_BOOT_COUNT.to_string()));
entry.write(&install_path)?;
while self.boot_entries.0.len() >= self.max_entries {
let mut e = self.boot_entries.0.pop().unwrap();
e.remove()?;
}
// 0) if boot.conf does not exist, just write it. done.
// 1) if current boot.conf is not verified, just replace it. done.
// 2) rotate boot.conf to boot.1.conf
// 3) create new boot.conf entry
Ok(install_path)
}
fn install_kernel_path(&self) -> Result<PathBuf> {
let version = match self.new_kernel.version {
Some(v) => v,
None => bail!("new kernel does not have a version"),
};
let mut path = Path::new("/boot").join(format!("bzImage-{}", version));
for i in 1..5 {
if !path.exists() {
return Ok(path);
}
path = Path::new("/boot").join(format!("bzImage-{}-{}", version, i));
}
bail!("Unable to find unused name for new kernel")
}
// return kernel commandline from most recent boot entry.
// If no boot entries exist, return default kernel commandline
fn generate_options_line(&self) -> &str {
if let Some(entry) = self.boot_entries.0.first() {
entry.options.as_str()
} else {
DEFAULT_KERNEL_CMDLINE
}
}
}
#[derive(PartialEq,Ord,PartialOrd,Eq,Copy,Clone,Debug)]
pub struct KernelVersion {
version: u32,
major: u32,
minor: Option<u32>,
revision: Option<u32>,
}
impl KernelVersion {
// return a KernelVersion instance if the string can be parsed as
// a valid kernel version string. Otherwise return None
fn parse_from_str(s: &str) -> Option<KernelVersion> {
let mut split = s.split("-");
let fields = split.next()
.and_then(Self::parse_version_field);
let revision = split.next()
.and_then(|s| s.parse::<u32>().ok());
fields.map(|v| {
KernelVersion {
version: v.0,
major: v.1,
minor: v.2,
revision,
}
})
}
pub fn parse_from_path(path: &Path) -> Option<KernelVersion> {
Self::path_version_string(path)
.and_then(|s| Self::parse_from_str(&s))
}
/// Return version as a string without including revision
pub fn version(&self) -> String {
if let Some(minor) = self.minor {
format!("{}.{}.{}", self.version, self.major, minor)
} else {
format!("{}.{}", self.version, self.major)
}
}
// turn path such as /path/to/bzImage-1.2.3 into the string "1.2.3"
// If path does not have a filename or if there is no '-' character
// in filename, return None
fn path_version_string(path: &Path) -> Option<String> {
path.file_name()
.and_then(|fname| fname.to_str())
.and_then(|s| s.splitn(2, "-").nth(1))
.map(ToString::to_string)
}
fn parse_version_field(s: &str) -> Option<(u32,u32,Option<u32>)> {
let elems: Vec<u32> = s.split(".")
.flat_map(|s| s.parse::<u32>().ok())
.collect();
match elems.len() {
2 => Some((elems[0], elems[1], None)),
3 => Some((elems[0], elems[1], Some(elems[2]))),
_ => None,
}
}
}
impl fmt::Display for KernelVersion {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}.{}", self.version, self.major)?;
if let Some(minor) = self.minor {
write!(f, ".{}", minor)?;
}
if let Some(revision) = self.revision {
write!(f, "-{}", revision)?;
}
Ok(())
}
}
struct BootEntries(Vec<BootEntry>);
impl BootEntries {
const BASE_PATH: &'static str = "/boot/loader/entries";
// The directory where boot entries are found
fn base_path() -> &'static Path {
Path::new(Self::BASE_PATH)
}
fn load() -> Result<BootEntries> {
let mut entries = BootEntries(Vec::new());
entries.load_entries()?;
Ok(entries)
}
fn load_entries(&mut self) -> Result<()> {
let base_path = Self::base_path();
if !base_path.exists() {
return Ok(())
}
for dirent in fs::read_dir(base_path)? {
let dirent = dirent?;
if let Some(fname) = dirent.file_name().to_str() {
self.load_filename(fname);
}
}
Ok(())
}
fn load_filename(&mut self, fname: &str) {
if fname.ends_with(".conf") {
let mut entry = BootEntry::from_filename(fname);
if let Err(e) = entry.load() {
warn!("Error loading boot entry {}: {}", fname, e);
} else {
self.0.push(entry);
}
}
}
fn find_by_name(&self, name: &str) -> BootEntries {
let mut v: Vec<BootEntry> = self.0.iter()
.filter(|e| e.name.as_str() == name)
.cloned()
.collect();
v.sort_by(|a,b| a.index.cmp(&b.index));
BootEntries(v)
}
// Rename entries in a series so that the base name
// (the name with no associated index value) is unused.
// so if boot.conf and boot.1.conf exist, they will
// be renamed to:
// boot.1.conf and boot.2.conf
fn rotate(&mut self) -> Result<()> {
if let Some(entry) = self.0.first() {
// Only rotate if the first entry:
// 1) exists
// 2) does not have an index value
// 3) does not have boot count (ie: in 'good' boot state)
if entry.index.is_none() && entry.is_good() {
self._rotate()?;
}
}
Ok(())
}
fn _rotate(&mut self) -> Result<()> {
for entry in self.0.iter_mut().rev() {
if !entry.rotate()? {
bail!("Failed to rotate boot entry {} because next index already exists", entry.path().display());
}
}
Ok(())
}
}
#[derive(Clone)]
struct BootEntry {
// The filename with index,bootcount,and suffix removed
name: String,
// An optional integer value parsed from filename
index: Option<u32>,
// See systemd-boot(7) for description of boot count name convention
boot_count: Option<String>,
// Contents of the 'title' line
title: String,
// The kernel image corresponding to the 'linux' line, if it exists
bzimage: Option<KernelBzImage>,
// Contents of the 'options' line
options: String,
}
impl BootEntry {
// parse filename into 3 components:
//
// Only the name field is mandatory. The index or bootcount may not exist.
//
// $(name).$(index)+$(bootcount).conf
//
// boot.2+3.conf ("boot", Some(2), Some("3"))
// boot.conf ("boot", None, None)
// boot+2-2.conf ("boot", None, Some("2-2"))
//
fn parse_filename(filename: &str) -> (String, Option<u32>, Option<String>) {
let filename = filename.trim_end_matches(".conf");
let mut parts = filename.splitn(2, '+');
let name = parts.next().unwrap().to_string();
let boot_count = parts.next().map(|s| s.to_string());
let v: Vec<&str> = name.rsplitn(2, '.').collect();
if v.len() == 2 {
if let Ok(n) = v[0].parse::<u32>() {
let index = Some(n);
let name = v[1].to_string();
return (name, index, boot_count)
}
}
(name, None, boot_count)
}
fn from_filename(filename: &str) -> BootEntry {
let (name, index, boot_count) = Self::parse_filename(filename);
Self::new(name, index, boot_count)
}
fn new<S: AsRef<str>>(name: S, index: Option<u32>, boot_count: Option<String>) -> BootEntry {
let name = name.as_ref().to_string();
BootEntry {
name, index, boot_count,
title: String::new(),
bzimage: None,
options: String::new(),
}
}
fn create_for_kernel(name: &str, kernel: KernelBzImage, options: &str, boot_count: Option<String>) -> BootEntry {
let mut entry = BootEntry::new(name, None, boot_count);
entry.options = options.to_string();
entry.generate_title(&kernel);
entry.bzimage = Some(kernel);
entry
}
fn write(&self, kernel_path: &Path) -> Result<()> {
let kernel = if let Some(fname) = kernel_path.file_name() {
fname.to_str().expect("could not convert filename to string").to_string()
} else {
bail!("kernel path does not have filename");
};
let mut buffer = String::new();
writeln!(&mut buffer, "title {}", self.title)?;
writeln!(&mut buffer, "linux /{}", kernel)?;
writeln!(&mut buffer, "options {}", self.options)?;
fs::write(self.path(), buffer)?;
Ok(())
}
fn is_good(&self) -> bool {
self.boot_count.is_none()
}
fn generate_title(&mut self, kernel: &KernelBzImage) {
if let Some(v) = kernel.version {
self.title = format!("Subgraph OS (Citadel {})", v);
} else {
self.title = format!("Subgraph OS (Citadel)");
}
}
fn load(&mut self) -> Result<()> {
let path = self.path();
for line in fs::read_to_string(&path)?.lines() {
if line.starts_with("title ") {
self.title = line.trim_start_matches("title ").to_owned();
} else if line.starts_with("linux /") {
let path = Path::new("/boot").join(line.trim_start_matches("linux /"));
if path.exists() {
let bzimage = KernelBzImage::from_path(&path)?;
self.bzimage = Some(bzimage);
} else {
bail!("kernel path {} in boot entry does not exist", path.display());
}
} else if line.starts_with("options ") {
self.options = line.trim_start_matches("options ").to_owned();
} else {
warn!("unexpected line in boot entry file {}: {}", path.display(), line);
}
}
if self.title.is_empty() {
bail!("no 'title' line in boot entry file {}", path.display());
}
if self.bzimage.is_none() {
bail!("no 'linux' line in boot entry file {}", path.display());
}
if self.options.is_empty() {
bail!("no 'options' line in boot entry file {}", path.display());
}
Ok(())
}
fn path(&self) -> PathBuf {
let mut filename = self.name.clone();
if let Some(index) = self.index {
filename.push_str(&format!(".{}", index));
}
if let Some(ref count) = self.boot_count {
filename.push_str(&format!("+{}.conf", count));
} else {
filename.push_str(".conf");
}
BootEntries::base_path().join(filename)
}
// Increment index value and rename boot entry file. Return false
// if new name already exists.
fn rotate(&mut self) -> Result<(bool)> {
let old_path = self.path();
let old_index = self.index;
self.index = match self.index {
Some(idx) => Some(idx + 1),
None => Some(1),
};
let new_path = self.path();
if new_path.exists() {
self.index = old_index;
return Ok(false);
}
verbose!("Rotating boot entry {} to {}", old_path.display(), new_path.display());
fs::rename(old_path, new_path)?;
Ok(true)
}
// Remove boot entry file and associated kernel bzimage
fn remove(&mut self) -> Result<()> {
if let Some(ref bzimage) = self.bzimage {
bzimage.remove_file()?;
self.bzimage = None;
}
fs::remove_file(self.path())?;
Ok(())
}
}
#[derive(Clone,PartialEq)]
struct KernelBzImage {
path: PathBuf,
version: Option<KernelVersion>,
shasum: String,
}
impl KernelBzImage {
fn from_path_and_version(path: PathBuf, version: &str) -> Result<KernelBzImage> {
let shasum = util::sha256(&path)?;
let version = KernelVersion::parse_from_str(version);
Ok(KernelBzImage {
path, version, shasum
})
}
fn from_path(path: &Path) -> Result<KernelBzImage> {
let version = KernelVersion::parse_from_path(&path);
let shasum = util::sha256(path)?;
let path = path.to_path_buf();
Ok(KernelBzImage { path, version, shasum })
}
fn remove_file(&self) -> Result<()> {
fs::remove_file(&self.path)?;
Ok(())
}
}
#[test]
fn test_version_parse() {
let path = Path::new("/boot/bzImage-2.2-x");
let kv = KernelVersion::parse_from_path(path).unwrap();
assert_eq!(kv.version, 2);
assert_eq!(kv.major, 2);
assert_eq!(kv.minor, None);
let kv2 = KernelVersion::parse_from_str("5.1.1").unwrap();
let kv3 = KernelVersion::parse_from_str("5.8.1").unwrap();
let kv4 = KernelVersion::parse_from_str("5.8").unwrap();
assert!(kv < kv2);
assert!(kv2 < kv3);
assert!(kv4 < kv3);
println!("{} {} {} {}", kv, kv2, kv3, kv4);
}
#[test]
fn test_bootentry_parse_filename() {
let fields = BootEntry::parse_filename("foo.heh.2+abc.conf");
assert_eq!(fields, ("foo.heh".to_string(), Some(2), Some("abc".to_string())));
let fields = BootEntry::parse_filename("foo+abc.conf");
assert_eq!(fields, ("foo".to_string(), None, Some("abc".to_string())));
let fields = BootEntry::parse_filename("foo.2.conf");
assert_eq!(fields, ("foo".to_string(), Some(2), None));
}

View File

@ -0,0 +1,338 @@
use std::path::{Path, PathBuf};
use std::fs;
use libcitadel::{Result, Partition, ResourceImage, ImageHeader, LogLevel, Logger};
use crate::update::kernel::{KernelInstaller, KernelVersion};
use std::collections::HashSet;
use std::fs::DirEntry;
mod kernel;
const FLAG_SKIP_SHA: u32 = 0x01;
const FLAG_NO_PREFER: u32 = 0x02;
const FLAG_QUIET: u32 = 0x04;
pub fn main(args: Vec<String>) {
let mut args = args.iter().skip(1);
let mut flags = 0;
Logger::set_log_level(LogLevel::Info);
while let Some(arg) = args.next() {
if arg == "--skip-sha" {
flags |= FLAG_SKIP_SHA;
} else if arg == "--no-prefer" {
flags |= FLAG_NO_PREFER;
} else if arg == "--quiet" {
flags |= FLAG_QUIET;
Logger::set_log_level(LogLevel::Warn);
} else if arg == "--verbose" {
Logger::set_log_level(LogLevel::Debug);
} else if arg == "--choose-rootfs" {
let _ = choose_install_partition(true);
return;
} else {
let path = Path::new(arg);
if let Err(e) = install_image(path, flags) {
warn!("Update failed: {}", e);
}
}
}
}
// Search directory containing installed image files for an
// image file that has an identical shasum and abort the installation
// if a duplicate is found.
fn detect_duplicates(image: &ResourceImage) -> Result<()> {
let metainfo = image.metainfo();
let channel = metainfo.channel();
let shasum = metainfo.shasum();
validate_channel_name(&channel)?;
let resource_dir = Path::new("/storage/resources/")
.join(channel);
if !resource_dir.exists() {
return Ok(())
}
for dirent in fs::read_dir(resource_dir)? {
let dirent = dirent?;
match ResourceImage::from_path(dirent.path()) {
Ok(img) => if img.metainfo().shasum() == shasum {
bail!("A duplicate image file with the same shasum already exists at {}", img.path().display());
},
Err(err) => warn!("{}", err),
}
}
Ok(())
}
fn install_image(path: &Path, flags: u32) -> Result<()> {
if !path.exists() {
bail!("file path {} does not exist", path.display());
}
let mut image = ResourceImage::from_path(path)?;
detect_duplicates(&image)?;
prepare_image(&image, flags)?;
match image.metainfo().image_type() {
"kernel" => install_kernel_image(&mut image),
"extra" => install_extra_image(&image),
"rootfs" => install_rootfs_image(&image, flags),
image_type => bail!("Unknown image type: {}", image_type),
}
}
// Prepare the image file for installation by decompressing and generating
// dmverity hash tree.
fn prepare_image(image: &ResourceImage, flags: u32) -> Result<()> {
if image.is_compressed() {
image.decompress()?;
}
if flags & FLAG_SKIP_SHA == 0 {
info!("Verifying sha256 hash of image");
let shasum = image.generate_shasum()?;
if shasum != image.metainfo().shasum() {
bail!("image file does not have expected sha256 value");
}
}
if !image.has_verity_hashtree() {
image.generate_verity_hashtree()?;
}
Ok(())
}
fn install_extra_image(image: &ResourceImage) -> Result<()> {
let filename = format!("citadel-extra-{:03}.img", image.header().metainfo().version());
install_image_file(image, filename.as_str())?;
remove_old_extra_images(image)?;
Ok(())
}
fn remove_old_extra_images(image: &ResourceImage) -> Result<()> {
let new_meta = image.header().metainfo();
let shasum = new_meta.shasum();
let target_dir = target_directory(image)?;
for dirent in fs::read_dir(target_dir)? {
let dirent = dirent?;
let path = dirent.path();
maybe_remove_old_extra_image(&path, shasum)?;
}
Ok(())
}
fn maybe_remove_old_extra_image(path: &Path, shasum: &str) -> Result<()> {
let header = ImageHeader::from_file(&path)?;
if !header.is_magic_valid() {
return Ok(());
}
let meta = header.metainfo();
if meta.image_type() != "extra" {
return Ok(());
}
if meta.shasum() != shasum {
info!("Removing old extra resource image {}", path.display());
fs::remove_file(&path)?;
}
Ok(())
}
fn install_kernel_image(image: &mut ResourceImage) -> Result<()> {
if !Path::new("/boot/loader/loader.conf").exists() {
bail!("failed to automount /boot partition. Please manually mount correct partition.");
}
let metainfo = image.header().metainfo();
let version = metainfo.version();
let kernel_version = match metainfo.kernel_version() {
Some(kv) => kv,
None => bail!("Kernel image does not have kernel version field"),
};
info!("kernel version is {}", kernel_version);
install_kernel_file(image, &kernel_version)?;
let filename = format!("citadel-kernel-{}-{:03}.img", kernel_version, version);
install_image_file(image, &filename)?;
let all_versions = all_boot_kernel_versions()?;
let image_dir = target_directory(image)?;
let mut remove_paths = Vec::new();
for dirent in fs::read_dir(image_dir)? {
let dirent = dirent?;
let path = dirent.path();
if is_unused_kernel_image(&path, &all_versions)? {
remove_paths.push(path);
}
}
for p in remove_paths {
fs::remove_file(p)?;
}
Ok(())
}
fn is_unused_kernel_image(path: &Path, versions: &HashSet<String>) -> Result<bool> {
let header = ImageHeader::from_file(path)?;
if !header.is_magic_valid() {
return Ok(false);
}
let meta = header.metainfo();
if meta.image_type() != "kernel" {
return Ok(false);
}
if let Some(version) = meta.kernel_version() {
if !versions.contains(version) {
info!("Removing kernel image {} because kernel version {} is unused", path.display(), version);
return Ok(true);
}
} else {
warn!("kernel image {} does not have kernel-version metainfo field", path.display());
}
Ok(false)
}
fn install_kernel_file(image: &mut ResourceImage, kernel_version: &str) -> Result<()> {
let mountpoint = Path::new("/run/citadel/images/kernel-install.mountpoint");
info!("Temporarily mounting kernel resource image");
let mut handle = image.mount_at(mountpoint)?;
let kernel_path = mountpoint.join("kernel/bzImage");
if !kernel_path.exists() {
handle.unmount()?;
bail!("kernel not found in kernel resource image at /kernel/bzImage")
}
let result = KernelInstaller::install_kernel(&kernel_path, kernel_version);
info!("Unmounting kernel resource image");
handle.unmount()?;
result
}
fn all_boot_kernel_versions() -> Result<HashSet<String>> {
let mut result = HashSet::new();
for dirent in fs::read_dir("/boot")? {
let dirent = dirent?;
if is_kernel_dirent(&dirent) {
if let Some(kv) = KernelVersion::parse_from_path(&dirent.path()) {
result.insert(kv.version());
}
}
}
Ok(result)
}
fn is_kernel_dirent(dirent: &DirEntry) -> bool {
if let Some(fname) = dirent.file_name().to_str() {
fname.starts_with("bzImage-")
} else {
false
}
}
fn install_image_file(image: &ResourceImage, filename: &str) -> Result<()> {
let image_dir = target_directory(image)?;
let image_dest = image_dir.join(filename);
if image_dest.exists() {
rotate(&image_dest)?;
}
info!("installing image file by moving from {} to {}", image.path().display(), image_dest.display());
fs::rename(image.path(), image_dest)?;
Ok(())
}
fn target_directory(image: &ResourceImage) -> Result<PathBuf> {
let metainfo = image.header().metainfo();
let channel = metainfo.channel();
validate_channel_name(channel)?;
Ok(Path::new("/storage/resources").join(channel))
}
fn rotate(path: &Path) -> Result<()> {
if !path.exists() || path.file_name().is_none() {
return Ok(());
}
let filename = path.file_name().unwrap();
let dot_zero = path.with_file_name(format!("{}.0", filename.to_string_lossy()));
if dot_zero.exists() {
fs::remove_file(&dot_zero)?;
}
fs::rename(path, &dot_zero)?;
Ok(())
}
fn validate_channel_name(channel: &str) -> Result<()> {
if !channel.chars().all(|c| c.is_ascii_lowercase()) {
bail!("Image has invalid channel name '{}'", channel);
}
Ok(())
}
fn install_rootfs_image(image: &ResourceImage, flags: u32) -> Result<()> {
let quiet = flags & FLAG_QUIET != 0;
let partition = choose_install_partition(!quiet)?;
if flags & FLAG_NO_PREFER == 0 {
clear_prefer_boot()?;
image.header().set_flag(ImageHeader::FLAG_PREFER_BOOT);
}
image.write_to_partition(&partition)?;
info!("Image written to {:?}", partition.path());
Ok(())
}
fn clear_prefer_boot() -> Result<()> {
for mut p in Partition::rootfs_partitions()? {
if p.is_initialized() && p.header().has_flag(ImageHeader::FLAG_PREFER_BOOT) {
p.clear_flag_and_write(ImageHeader::FLAG_PREFER_BOOT)?;
}
}
Ok(())
}
fn bool_to_yesno(val: bool) -> &'static str {
if val {
"YES"
} else {
" NO"
}
}
fn choose_install_partition(verbose: bool) -> Result<Partition> {
let partitions = Partition::rootfs_partitions()?;
if verbose {
for p in &partitions {
info!("Partition: {} (Mounted: {}) (Empty: {})",
p.path().display(),
bool_to_yesno(p.is_mounted()),
bool_to_yesno(!p.is_initialized()));
}
}
for p in &partitions {
if !p.is_mounted() && !p.is_initialized() {
if verbose {
info!("Choosing {} because it is empty and not mounted", p.path().display());
}
return Ok(p.clone())
}
}
for p in &partitions {
if !p.is_mounted() {
if verbose {
info!("Choosing {} because it is not mounted", p.path().display());
info!("Header metainfo:");
print!("{}",String::from_utf8(p.header().metainfo_bytes())?);
}
return Ok(p.clone())
}
}
Err(format_err!("No suitable install partition found"))
}