mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
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:
4
core/Cargo.lock
generated
4
core/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
|
||||
Reference in New Issue
Block a user