diff --git a/citadel-tool/src/main.rs b/citadel-tool/src/main.rs index cae6993..706a388 100644 --- a/citadel-tool/src/main.rs +++ b/citadel-tool/src/main.rs @@ -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) { "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)), diff --git a/citadel-tool/src/update/kernel.rs b/citadel-tool/src/update/kernel.rs new file mode 100644 index 0000000..d73c7d6 --- /dev/null +++ b/citadel-tool/src/update/kernel.rs @@ -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 { + 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 { + 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 { + 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, + revision: Option, +} + +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 { + let mut split = s.split("-"); + + let fields = split.next() + .and_then(Self::parse_version_field); + + let revision = split.next() + .and_then(|s| s.parse::().ok()); + + fields.map(|v| { + KernelVersion { + version: v.0, + major: v.1, + minor: v.2, + revision, + } + }) + } + + pub fn parse_from_path(path: &Path) -> Option { + 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 { + 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)> { + let elems: Vec = s.split(".") + .flat_map(|s| s.parse::().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); + +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 { + 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 = 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, + // See systemd-boot(7) for description of boot count name convention + boot_count: Option, + // Contents of the 'title' line + title: String, + // The kernel image corresponding to the 'linux' line, if it exists + bzimage: Option, + // 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, Option) { + 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::() { + 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>(name: S, index: Option, boot_count: Option) -> 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) -> 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, + shasum: String, +} + +impl KernelBzImage { + fn from_path_and_version(path: PathBuf, version: &str) -> Result { + let shasum = util::sha256(&path)?; + let version = KernelVersion::parse_from_str(version); + Ok(KernelBzImage { + path, version, shasum + }) + } + + fn from_path(path: &Path) -> Result { + 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)); +} \ No newline at end of file diff --git a/citadel-tool/src/update/mod.rs b/citadel-tool/src/update/mod.rs new file mode 100644 index 0000000..2ab881b --- /dev/null +++ b/citadel-tool/src/update/mod.rs @@ -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) { + 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) -> Result { + 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> { + 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 { + 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 { + 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")) +}