redesign firmware updater (#2521)

* bump version

* Update image-recipe/build.sh

* fix podman repo

* improve firmware updater

Co-authored-by: J H <Blu-J@users.noreply.github.com>

* checksum firmware

* include sha in json

* fix build

* fix semver parser, add rpc for manual trigger

---------

Co-authored-by: J H <Blu-J@users.noreply.github.com>
This commit is contained in:
Aiden McClelland
2023-11-16 15:49:03 -07:00
committed by GitHub
parent 0b43aab855
commit 749cde13c4
13 changed files with 201 additions and 56 deletions

4
core/Cargo.lock generated
View File

@@ -4238,6 +4238,9 @@ name = "semver"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
dependencies = [
"serde",
]
[[package]]
name = "semver-parser"
@@ -5000,6 +5003,7 @@ dependencies = [
"rpc-toolkit",
"rust-argon2",
"scopeguard",
"semver 1.0.20",
"serde",
"serde_json",
"serde_with",

View File

@@ -128,6 +128,7 @@ rpassword = "7.2.0"
rpc-toolkit = "0.2.2"
rust-argon2 = "2.0.0"
scopeguard = "1.1" # because avahi-sys fucks your shit up
semver = { version = "1.0.20", features = ["serde"] }
serde = { version = "1.0", features = ["derive", "rc"] }
serde_cbor = { package = "ciborium", version = "0.2.1" }
serde_json = "1.0"

View File

@@ -9,7 +9,7 @@ use tracing::instrument;
use crate::context::rpc::RpcContextConfig;
use crate::context::{DiagnosticContext, InstallContext, SetupContext};
use crate::disk::fsck::RepairStrategy;
use crate::disk::fsck::{RepairStrategy, RequiresReboot};
use crate::disk::main::DEFAULT_PASSWORD;
use crate::disk::REPAIR_DISK_PATH;
use crate::firmware::update_firmware;
@@ -30,11 +30,18 @@ async fn setup_or_init(cfg_path: Option<PathBuf>) -> Result<Option<Shutdown>, Er
}
}));
if update_firmware().await?.0 {
return Ok(Some(Shutdown {
export_args: None,
restart: true,
}));
match update_firmware().await {
Ok(RequiresReboot(true)) => {
return Ok(Some(Shutdown {
export_args: None,
restart: true,
}))
}
Err(e) => {
tracing::warn!("Error performing firmware update: {e}");
tracing::debug!("{e:?}");
}
_ => (),
}
Command::new("ln")

View File

@@ -11,7 +11,7 @@ use crate::Error;
pub mod btrfs;
pub mod ext4;
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
#[must_use]
pub struct RequiresReboot(pub bool);
impl std::ops::BitOrAssign for RequiresReboot {

View File

@@ -1,16 +1,63 @@
use std::collections::BTreeSet;
use std::path::Path;
use async_compression::tokio::bufread::GzipDecoder;
use clap::ArgMatches;
use rpc_toolkit::command;
use serde::{Deserialize, Serialize};
use tokio::fs::File;
use tokio::io::{AsyncRead, BufReader};
use tokio::io::BufReader;
use tokio::process::Command;
use crate::disk::fsck::RequiresReboot;
use crate::prelude::*;
use crate::util::Invoke;
use crate::PLATFORM;
/// Part of the Firmware, look there for more about
#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct VersionMatcher {
/// Strip this prefix on the version matcher
semver_prefix: Option<String>,
/// Match the semver to this range
semver_range: Option<semver::VersionReq>,
/// Strip this suffix on the version matcher
semver_suffix: Option<String>,
}
/// Inside a file that is firmware.json, we
/// wanted a structure that could help decide what to do
/// for each of the firmware versions
#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Firmware {
id: String,
/// This is the platform(s) the firmware was built for
platform: BTreeSet<String>,
/// This usally comes from the dmidecode
system_product_name: Option<String>,
/// The version comes from dmidecode, then we decide if it matches
bios_version: Option<VersionMatcher>,
/// the hash of the firmware rom.gz
shasum: String,
}
fn display_firmware_update_result(arg: RequiresReboot, _: &ArgMatches) {
if arg.0 {
println!("Firmware successfully updated! Reboot to apply changes.");
} else {
println!("No firmware update available.");
}
}
/// We wanted to make sure during every init
/// that the firmware was the correct and updated for
/// systems like the Pure System that a new firmware
/// was released and the updates where pushed through the pure os.
#[command(rename = "update-firmware", display(display_firmware_update_result))]
pub async fn update_firmware() -> Result<RequiresReboot, Error> {
let product_name = String::from_utf8(
let system_product_name = String::from_utf8(
Command::new("dmidecode")
.arg("-s")
.arg("system-product-name")
@@ -19,52 +66,84 @@ pub async fn update_firmware() -> Result<RequiresReboot, Error> {
)?
.trim()
.to_owned();
if product_name.is_empty() {
let bios_version = String::from_utf8(
Command::new("dmidecode")
.arg("-s")
.arg("bios-version")
.invoke(ErrorKind::Firmware)
.await?,
)?
.trim()
.to_owned();
if system_product_name.is_empty() || bios_version.is_empty() {
return Ok(RequiresReboot(false));
}
let firmware_dir = Path::new("/usr/lib/startos/firmware").join(&product_name);
if tokio::fs::metadata(&firmware_dir).await.is_ok() {
let current_firmware = String::from_utf8(
Command::new("dmidecode")
.arg("-s")
.arg("bios-version")
.invoke(ErrorKind::Firmware)
.await?,
)?
.trim()
.to_owned();
if tokio::fs::metadata(firmware_dir.join(format!("{current_firmware}.rom.gz")))
.await
.is_err()
&& tokio::fs::metadata(firmware_dir.join(format!("{current_firmware}.rom")))
.await
.is_err()
{
let mut firmware_read_dir = tokio::fs::read_dir(&firmware_dir).await?;
while let Some(entry) = firmware_read_dir.next_entry().await? {
let filename = entry.file_name().to_string_lossy().into_owned();
let rdr: Option<Box<dyn AsyncRead + Unpin + Send>> =
if filename.ends_with(".rom.gz") {
Some(Box::new(GzipDecoder::new(BufReader::new(
File::open(entry.path()).await?,
))))
} else if filename.ends_with(".rom") {
Some(Box::new(File::open(entry.path()).await?))
} else {
None
};
if let Some(mut rdr) = rdr {
Command::new("flashrom")
.arg("-p")
.arg("internal")
.arg("-w-")
.input(Some(&mut rdr))
.invoke(ErrorKind::Firmware)
.await?;
return Ok(RequiresReboot(true));
let firmware_dir = Path::new("/usr/lib/startos/firmware");
for firmware in serde_json::from_str::<Vec<Firmware>>(
&tokio::fs::read_to_string("/usr/lib/startos/firmware.json").await?,
)
.with_kind(ErrorKind::Deserialization)?
{
let id = firmware.id;
let matches_product_name = firmware
.system_product_name
.map_or(true, |spn| spn == system_product_name);
let matches_bios_version = firmware
.bios_version
.map_or(Some(true), |bv| {
let mut semver_str = bios_version.as_str();
if let Some(prefix) = &bv.semver_prefix {
semver_str = semver_str.strip_prefix(prefix)?;
}
}
if let Some(suffix) = &bv.semver_suffix {
semver_str = semver_str.strip_suffix(suffix)?;
}
let semver = semver_str
.split(".")
.filter_map(|v| v.parse().ok())
.chain(std::iter::repeat(0))
.take(3)
.collect::<Vec<_>>();
let semver = semver::Version::new(semver[0], semver[1], semver[2]);
Some(
bv.semver_range
.as_ref()
.map_or(true, |r| r.matches(&semver)),
)
})
.unwrap_or(false);
if firmware.platform.contains(&*PLATFORM) && matches_product_name && matches_bios_version {
let filename = format!("{id}.rom.gz");
let firmware_path = firmware_dir.join(&filename);
Command::new("sha256sum")
.arg("-c")
.input(Some(&mut std::io::Cursor::new(format!(
"{} {}",
firmware.shasum,
firmware_path.display()
))))
.invoke(ErrorKind::Filesystem)
.await?;
let mut rdr = if tokio::fs::metadata(&firmware_path).await.is_ok() {
GzipDecoder::new(BufReader::new(File::open(&firmware_path).await?))
} else {
return Err(Error::new(
eyre!("Firmware {id}.rom.gz not found in {firmware_dir:?}"),
ErrorKind::NotFound,
));
};
Command::new("flashrom")
.arg("-p")
.arg("internal")
.arg("-w-")
.input(Some(&mut rdr))
.invoke(ErrorKind::Firmware)
.await?;
return Ok(RequiresReboot(true));
}
}
Ok(RequiresReboot(false))
}

View File

@@ -105,6 +105,7 @@ pub fn main_api() -> Result<(), RpcError> {
shutdown::restart,
shutdown::rebuild,
update::update_system,
firmware::update_firmware,
))]
pub fn server() -> Result<(), RpcError> {
Ok(())