use anyhow::{Context, Result};
use fn_error_context::context;
use openat_ext::OpenatDirExt;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use crate::{bootupd::RootContext, model::*};
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum ValidationResult {
Valid,
Skip,
Errors(Vec<String>),
}
pub(crate) trait Component {
fn name(&self) -> &'static str;
fn query_adopt(&self, devices: &Option<Vec<String>>) -> Result<Option<Adoptable>>;
fn migrate_static_grub_config(&self, sysroot_path: &str, destdir: &openat::Dir) -> Result<()>;
fn adopt_update(
&self,
rootcxt: &RootContext,
update: &ContentMetadata,
with_static_config: bool,
) -> Result<Option<InstalledContent>>;
fn install(
&self,
src_root: &str,
dest_root: &str,
device: &str,
update_firmware: bool,
) -> Result<InstalledContent>;
fn generate_update_metadata(&self, sysroot: &str) -> Result<ContentMetadata>;
fn query_update(&self, sysroot: &openat::Dir) -> Result<Option<ContentMetadata>>;
fn run_update(
&self,
rootcxt: &RootContext,
current: &InstalledContent,
) -> Result<InstalledContent>;
fn validate(&self, current: &InstalledContent) -> Result<ValidationResult>;
fn get_efi_vendor(&self, sysroot: &Path) -> Result<Option<String>>;
}
pub(crate) fn new_from_name(name: &str) -> Result<Box<dyn Component>> {
let r: Box<dyn Component> = match name {
#[cfg(any(
target_arch = "x86_64",
target_arch = "aarch64",
target_arch = "riscv64"
))]
#[allow(clippy::box_default)]
"EFI" => Box::new(crate::efi::Efi::default()),
#[cfg(any(target_arch = "x86_64", target_arch = "powerpc64"))]
#[allow(clippy::box_default)]
"BIOS" => Box::new(crate::bios::Bios::default()),
_ => anyhow::bail!("No component {}", name),
};
Ok(r)
}
#[cfg(any(
target_arch = "x86_64",
target_arch = "aarch64",
target_arch = "riscv64"
))]
pub(crate) fn component_updatedirname(component: &dyn Component) -> PathBuf {
Path::new(BOOTUPD_UPDATES_DIR).join(component.name())
}
#[cfg(any(
target_arch = "x86_64",
target_arch = "aarch64",
target_arch = "riscv64"
))]
pub(crate) fn component_updatedir(sysroot: &str, component: &dyn Component) -> PathBuf {
Path::new(sysroot).join(component_updatedirname(component))
}
fn component_update_data_name(component: &dyn Component) -> PathBuf {
Path::new(&format!("{}.json", component.name())).into()
}
pub(crate) fn write_update_metadata(
sysroot: &str,
component: &dyn Component,
meta: &ContentMetadata,
) -> Result<()> {
let sysroot = openat::Dir::open(sysroot)?;
let dir = sysroot.sub_dir(BOOTUPD_UPDATES_DIR)?;
let name = component_update_data_name(component);
dir.write_file_with(name, 0o644, |w| -> Result<_> {
Ok(serde_json::to_writer(w, &meta)?)
})?;
Ok(())
}
#[context("Loading update for component {}", component.name())]
pub(crate) fn get_component_update(
sysroot: &openat::Dir,
component: &dyn Component,
) -> Result<Option<ContentMetadata>> {
let name = component_update_data_name(component);
let path = Path::new(BOOTUPD_UPDATES_DIR).join(name);
if let Some(f) = sysroot.open_file_optional(&path)? {
let mut f = std::io::BufReader::new(f);
let u = serde_json::from_reader(&mut f)
.with_context(|| format!("failed to parse {:?}", &path))?;
Ok(Some(u))
} else {
Ok(None)
}
}
#[context("Querying adoptable state")]
pub(crate) fn query_adopt_state() -> Result<Option<Adoptable>> {
if let Some(coreos_aleph) = crate::coreos::get_aleph_version(Path::new("/"))? {
let meta = ContentMetadata {
timestamp: coreos_aleph.ts,
version: coreos_aleph.aleph.version,
versions: None,
};
log::trace!("Adoptable: {:?}", &meta);
return Ok(Some(Adoptable {
version: meta,
confident: true,
}));
} else {
log::trace!("No CoreOS aleph detected");
}
let ostree_deploy_dir = Path::new("/ostree/deploy");
if ostree_deploy_dir.exists() {
let btime = ostree_deploy_dir.metadata()?.created()?;
let timestamp = chrono::DateTime::from(btime);
let meta = ContentMetadata {
timestamp,
version: "unknown".to_string(),
versions: None,
};
return Ok(Some(Adoptable {
version: meta,
confident: true,
}));
}
Ok(None)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_efi_vendor() -> Result<()> {
let td = tempfile::tempdir()?;
let tdp = td.path();
let tdupdates = "usr/lib/bootupd/updates/EFI";
let tdir = openat::Dir::open(tdp)?;
tdir.ensure_dir_all(tdupdates, 0o755)?;
let efi = tdir.sub_dir(tdupdates)?;
efi.create_dir("BOOT", 0o755)?;
efi.create_dir("fedora", 0o755)?;
efi.create_dir("centos", 0o755)?;
efi.write_file_contents(
format!("fedora/{}", crate::efi::SHIM),
0o644,
"shim data".as_bytes(),
)?;
efi.write_file_contents(
format!("centos/{}", crate::efi::SHIM),
0o644,
"shim data".as_bytes(),
)?;
let all_components = crate::bootupd::get_components();
let target_components: Vec<_> = all_components.values().collect();
for &component in target_components.iter() {
if component.name() == "BIOS" {
assert_eq!(component.get_efi_vendor(tdp)?, None);
}
if component.name() == "EFI" {
let x = component.get_efi_vendor(tdp);
assert_eq!(x.is_err(), true);
efi.remove_all("centos")?;
assert_eq!(component.get_efi_vendor(tdp)?, Some("fedora".to_string()));
{
let td_vendor = "usr/lib/efi/shim/15.8-3/EFI/centos";
tdir.ensure_dir_all(td_vendor, 0o755)?;
let shim_dir = tdir.sub_dir(td_vendor)?;
shim_dir.write_file_contents(
crate::efi::SHIM,
0o644,
"shim data".as_bytes(),
)?;
assert_eq!(component.get_efi_vendor(tdp)?, Some("centos".to_string()));
let td_usr = tdp.join("usr/lib/efi");
assert_eq!(
component.get_efi_vendor(&td_usr)?,
Some("centos".to_string())
);
let td_efi = tdp.join(component_updatedirname(&**component));
assert_eq!(
component.get_efi_vendor(&td_efi)?,
Some("fedora".to_string())
);
tdir.remove_all("usr/lib/efi")?;
tdir.remove_all(tdupdates)?;
let err = component.get_efi_vendor(&td_usr).unwrap_err();
assert_eq!(err.to_string(), "Failed to find valid target path");
}
}
}
Ok(())
}
}