From 1e7a50db174e6389c36ef356b6a32b9b13890dec Mon Sep 17 00:00:00 2001 From: unitexe Date: Mon, 22 Dec 2025 13:23:47 -0600 Subject: A devinfod prototype - Get root - Get select u-boot environment variables - Get build info from /etc/buildinfo - Get versions from /etc/sw-versions --- src/main.rs | 393 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 393 insertions(+) create mode 100644 src/main.rs (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..5c34d73 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,393 @@ +use std::process::Command; +use std::str::FromStr; +use std::{fs, io}; + +use tonic::{Request, Response, Status, transport::Server}; + +use crate::devinfod::dev_info_server::{DevInfo, DevInfoServer}; +use crate::devinfod::{ + GetBuildInfoRequest, GetBuildInfoResponse, GetRootRequest, GetRootResponse, + GetSwVersionsRequest, GetSwVersionsResponse, GetUbootEnvRequest, GetUbootEnvResponse, +}; + +pub mod devinfod { + tonic::include_proto!("unit.info.v0"); +} + +#[derive(Debug, Default)] +pub struct MyDevInfo {} + +pub struct Distro { + name: String, + version: String, +} + +impl Distro { + pub fn to_pb(&self) -> devinfod::Distro { + devinfod::Distro { + name: self.name.clone(), + version: self.version.clone(), + } + } +} + +pub struct Layer { + name: String, + branch: String, + revision: String, +} + +impl Layer { + pub fn to_pb(&self) -> devinfod::Layer { + devinfod::Layer { + name: self.name.clone(), + branch: self.branch.clone(), + revision: self.revision.clone(), + } + } +} + +enum BuildInfoSection { + BuildConfiguration, + LayerRevisions, +} + +impl BuildInfoSection { + pub fn from_line(line: &str) -> Option { + if line.starts_with("Build Configuration") { + Some(BuildInfoSection::BuildConfiguration) + } else if line.starts_with("Layer Revisions") { + Some(BuildInfoSection::LayerRevisions) + } else { + None + } + } +} + +pub struct BuildInfo { + build_configuration: Distro, + layer_revisions: Vec, +} + +impl BuildInfo { + pub fn to_pb(&self) -> devinfod::BuildInfo { + let mut layers_pb = Vec::::new(); + + for layer in &self.layer_revisions { + layers_pb.push(layer.to_pb()); + } + + devinfod::BuildInfo { + distro: Some(self.build_configuration.to_pb()), + layers: layers_pb, + } + } +} + +impl FromStr for BuildInfo { + type Err = io::Error; + + fn from_str(s: &str) -> Result { + let mut distro_name: Option = None; + let mut distro_version: Option = None; + let mut layers = Vec::new(); + let mut curr_section: Option = None; + + for line in s.lines() { + let trimmed = line.trim(); + + if line.ends_with("|") { + if let Some(section) = BuildInfoSection::from_line(line) { + curr_section = Some(section); + continue; + } + } + + if line.starts_with("-----") { + continue; + } + + if let Some(section) = &curr_section { + match section { + BuildInfoSection::BuildConfiguration => { + if let Some((key, value)) = trimmed.split_once("=") { + let key = key.trim(); + let value = value.trim(); + + match key { + "DISTRO" => distro_name = Some(value.to_string()), + "DISTRO_VERSION" => distro_version = Some(value.to_string()), + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "Distro configuration contains unexpected key: {}", + key + ), + )); + } + } + } else { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Entry for build configuration has invalid format, should be of form = ", + )); + } + } + BuildInfoSection::LayerRevisions => { + if let Some((key, value)) = trimmed.split_once("=") { + let layer_name = key.trim(); + let value = value.trim(); + if let Some((branch, revision)) = value.split_once(":") { + layers.push(Layer { + name: layer_name.to_string(), + branch: branch.to_string(), + revision: revision.to_string(), + }); + } else { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "Entry for layer {} contains invalid value format, should be of form :", + layer_name + ), + )); + } + } else { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Entry for layer revisions has invalid format, should be of form = ", + )); + } + } + } + } + } + + match (distro_name, distro_version) { + (Some(name), Some(version)) => Ok(BuildInfo { + build_configuration: Distro { name, version }, + layer_revisions: layers, + }), + _ => Err(io::Error::new( + io::ErrorKind::InvalidData, + "Missing required build configuration information", + )), + } + } +} + +pub struct SwVersion { + name: String, + version: String, +} + +impl SwVersion { + pub fn to_pb(&self) -> devinfod::SwVersion { + devinfod::SwVersion { + name: self.name.clone(), + version: self.version.clone(), + } + } +} + +pub struct SwVersions { + versions: Vec, +} + +impl SwVersions { + pub fn to_pb(&self) -> devinfod::SwVersions { + let mut versions = Vec::::new(); + + for sw_version in &self.versions { + versions.push(sw_version.to_pb()); + } + + devinfod::SwVersions { versions } + } +} + +impl FromStr for SwVersions { + type Err = io::Error; + + fn from_str(s: &str) -> Result { + let mut versions = Vec::new(); + + for line in s.lines() { + let trimmed = line.trim(); + + if let Some((key, value)) = trimmed.split_once(" ") { + let key = key.trim(); + let value = value.trim(); + + versions.push(SwVersion { + name: key.to_string(), + version: value.to_string(), + }); + } else { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Entry for sw versions has invalid format, should be of form ", + )); + } + } + + Ok(SwVersions { versions }) + } +} + +fn swupdate_get_root() -> io::Result { + let output = Command::new("swupdate").arg("-g").output()?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + eprintln!("swupdate stderr: {}", stderr); + return Err(io::Error::new( + io::ErrorKind::Other, + format!("swupdate failed with status: {}", output.status), + )); + } + + let root = String::from_utf8(output.stdout) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))? + .trim() + .to_string(); + + Ok(root) +} + +struct EnvVar { + name: String, + value: String, +} + +impl EnvVar { + pub fn to_pb(&self) -> devinfod::EnvVar { + devinfod::EnvVar { + name: self.name.clone(), + value: self.value.clone(), + } + } +} + +struct UbootEnv { + vars: Vec, +} + +impl UbootEnv { + pub fn from_cmd() -> io::Result { + let output = Command::new("fw_printenv").output()?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + eprintln!("fw_printenv stderr: {}", stderr); + return Err(io::Error::new( + io::ErrorKind::Other, + format!("fw_printenv failed with status: {}", output.status), + )); + } + + let output_str = String::from_utf8(output.stdout) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))? + .trim() + .to_string(); + + let mut vars = Vec::::new(); + + for line in output_str.lines() { + let trimmed = line.trim(); + if let Some((key, value)) = trimmed.split_once("=") { + let key = key.trim(); + + if matches!( + key, + "target_partition_device_node" | "upgrade_available" | "ustate" + ) { + let value = value.trim(); + vars.push(EnvVar { + name: key.to_string(), + value: value.to_string(), + }) + } + } else { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Line for u-boot environment has invalid format, should be of form =", + )); + } + } + + Ok(UbootEnv { vars }) + } + + pub fn to_pb(&self) -> devinfod::UbootEnv { + let mut vars_pb = Vec::::new(); + + for var in &self.vars { + vars_pb.push(var.to_pb()); + } + + devinfod::UbootEnv { vars: vars_pb } + } +} + +#[tonic::async_trait] +impl DevInfo for MyDevInfo { + async fn get_build_info( + &self, + _request: Request, + ) -> Result, Status> { + let build_info: BuildInfo = fs::read_to_string("/etc/buildinfo")?.parse()?; + let build_info_pb = build_info.to_pb(); + let response = GetBuildInfoResponse { + info: Some(build_info_pb), + }; + Ok(Response::new(response)) + } + + async fn get_root( + &self, + _request: Request, + ) -> Result, Status> { + let root = swupdate_get_root()?; + let response = GetRootResponse { root }; + Ok(Response::new(response)) + } + + async fn get_sw_versions( + &self, + _request: Request, + ) -> Result, Status> { + let sw_versions: SwVersions = fs::read_to_string("/etc/sw-versions")?.parse()?; + let sw_versions_pb = sw_versions.to_pb(); + let response = GetSwVersionsResponse { + versions: Some(sw_versions_pb), + }; + Ok(Response::new(response)) + } + + async fn get_uboot_env( + &self, + _request: Request, + ) -> Result, Status> { + let env = UbootEnv::from_cmd()?; + let env_pb = env.to_pb(); + let response = GetUbootEnvResponse { env: Some(env_pb) }; + Ok(Response::new(response)) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let addr = "0.0.0.0:60067".parse().unwrap(); + let devinfo = MyDevInfo::default(); + + println!("Listening on {}", addr); + + Server::builder() + .add_service(DevInfoServer::new(devinfo)) + .serve(addr) + .await?; + + Ok(()) +} -- cgit v1.2.3