summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorunitexe <unitexe70@gmail.com>2025-12-22 13:23:47 -0600
committerunitexe <unitexe70@gmail.com>2025-12-22 13:25:11 -0600
commit1e7a50db174e6389c36ef356b6a32b9b13890dec (patch)
tree51396a29232ebaeba2f464ecd924fceb766963e4
A devinfod prototypeHEADmain
- Get root - Get select u-boot environment variables - Get build info from /etc/buildinfo - Get versions from /etc/sw-versions
-rw-r--r--.gitignore22
-rw-r--r--Cargo.toml13
-rw-r--r--LICENSE9
-rw-r--r--README.md3
-rw-r--r--build.rs12
-rw-r--r--proto/buildinfo.proto19
-rw-r--r--proto/devinfo.proto38
-rw-r--r--proto/swversions.proto12
-rw-r--r--proto/ubootenv.proto12
-rw-r--r--src/main.rs393
10 files changed, 533 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ab951f8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,22 @@
+# ---> Rust
+# Generated by Cargo
+# will have compiled files and executables
+debug/
+target/
+
+# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
+# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
+Cargo.lock
+
+# These are backup files generated by rustfmt
+**/*.rs.bk
+
+# MSVC Windows builds of rustc generate these, which store debugging information
+*.pdb
+
+# RustRover
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..43978f9
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "devinfod"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+prost = "0.14.1"
+tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread"] }
+tonic = "0.14.2"
+tonic-prost = "0.14.2"
+
+[build-dependencies]
+tonic-prost-build = "0.14.2"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..714c400
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,9 @@
+MIT License
+
+Copyright (c) 2025 unitexe
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7eb1acf
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# devinfod
+
+A gRPC server for querying device info \ No newline at end of file
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000..c5dbf1c
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,12 @@
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ tonic_prost_build::configure().compile_protos(
+ &[
+ "proto/buildinfo.proto",
+ "proto/devinfo.proto",
+ "proto/swversions.proto",
+ "proto/ubootenv.proto",
+ ],
+ &["proto/"],
+ )?;
+ Ok(())
+}
diff --git a/proto/buildinfo.proto b/proto/buildinfo.proto
new file mode 100644
index 0000000..2cf9dab
--- /dev/null
+++ b/proto/buildinfo.proto
@@ -0,0 +1,19 @@
+syntax = "proto3";
+
+package unit.info.v0;
+
+message Distro {
+ string name = 1;
+ string version = 2;
+}
+
+message Layer {
+ string name = 1;
+ string branch = 2;
+ string revision = 3;
+}
+
+message BuildInfo {
+ Distro distro = 1;
+ repeated Layer layers = 2;
+}
diff --git a/proto/devinfo.proto b/proto/devinfo.proto
new file mode 100644
index 0000000..5061ffa
--- /dev/null
+++ b/proto/devinfo.proto
@@ -0,0 +1,38 @@
+syntax = "proto3";
+
+package unit.info.v0;
+
+import "buildinfo.proto";
+import "ubootenv.proto";
+import "swversions.proto";
+
+service DevInfo {
+ rpc GetBuildInfo (GetBuildInfoRequest) returns (GetBuildInfoResponse) {}
+ rpc GetRoot (GetRootRequest) returns (GetRootResponse) {}
+ rpc GetSwVersions (GetSwVersionsRequest) returns (GetSwVersionsResponse) {}
+ rpc GetUbootEnv (GetUbootEnvRequest) returns (GetUbootEnvResponse) {}
+}
+
+message GetBuildInfoRequest {}
+
+message GetBuildInfoResponse {
+ BuildInfo info = 1;
+}
+
+message GetRootRequest {}
+
+message GetRootResponse {
+ string root = 1;
+}
+
+message GetSwVersionsRequest {}
+
+message GetSwVersionsResponse {
+ SwVersions versions = 1;
+}
+
+message GetUbootEnvRequest {}
+
+message GetUbootEnvResponse {
+ UbootEnv env = 1;
+}
diff --git a/proto/swversions.proto b/proto/swversions.proto
new file mode 100644
index 0000000..bf0ffb2
--- /dev/null
+++ b/proto/swversions.proto
@@ -0,0 +1,12 @@
+syntax = "proto3";
+
+package unit.info.v0;
+
+message SwVersion {
+ string name = 1;
+ string version = 2;
+}
+
+message SwVersions {
+ repeated SwVersion versions = 1;
+}
diff --git a/proto/ubootenv.proto b/proto/ubootenv.proto
new file mode 100644
index 0000000..ad201da
--- /dev/null
+++ b/proto/ubootenv.proto
@@ -0,0 +1,12 @@
+syntax = "proto3";
+
+package unit.info.v0;
+
+message EnvVar {
+ string name = 1;
+ string value = 2;
+}
+
+message UbootEnv {
+ repeated EnvVar vars = 1;
+}
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<Self> {
+ 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<Layer>,
+}
+
+impl BuildInfo {
+ pub fn to_pb(&self) -> devinfod::BuildInfo {
+ let mut layers_pb = Vec::<devinfod::Layer>::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<Self, Self::Err> {
+ let mut distro_name: Option<String> = None;
+ let mut distro_version: Option<String> = None;
+ let mut layers = Vec::new();
+ let mut curr_section: Option<BuildInfoSection> = 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 <key> = <value>",
+ ));
+ }
+ }
+ 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 <branch>:<revision>",
+ layer_name
+ ),
+ ));
+ }
+ } else {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidData,
+ "Entry for layer revisions has invalid format, should be of form <key> = <value>",
+ ));
+ }
+ }
+ }
+ }
+ }
+
+ 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<SwVersion>,
+}
+
+impl SwVersions {
+ pub fn to_pb(&self) -> devinfod::SwVersions {
+ let mut versions = Vec::<devinfod::SwVersion>::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<Self, Self::Err> {
+ 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 <key> <value>",
+ ));
+ }
+ }
+
+ Ok(SwVersions { versions })
+ }
+}
+
+fn swupdate_get_root() -> io::Result<String> {
+ 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<EnvVar>,
+}
+
+impl UbootEnv {
+ pub fn from_cmd() -> io::Result<Self> {
+ 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::<EnvVar>::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 <key>=<value>",
+ ));
+ }
+ }
+
+ Ok(UbootEnv { vars })
+ }
+
+ pub fn to_pb(&self) -> devinfod::UbootEnv {
+ let mut vars_pb = Vec::<devinfod::EnvVar>::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<GetBuildInfoRequest>,
+ ) -> Result<Response<GetBuildInfoResponse>, 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<GetRootRequest>,
+ ) -> Result<Response<GetRootResponse>, Status> {
+ let root = swupdate_get_root()?;
+ let response = GetRootResponse { root };
+ Ok(Response::new(response))
+ }
+
+ async fn get_sw_versions(
+ &self,
+ _request: Request<GetSwVersionsRequest>,
+ ) -> Result<Response<GetSwVersionsResponse>, 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<GetUbootEnvRequest>,
+ ) -> Result<Response<GetUbootEnvResponse>, 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<dyn std::error::Error>> {
+ 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(())
+}