diff --git a/Cargo.lock b/Cargo.lock index 545d6668c..2f3fe3e89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -618,6 +618,7 @@ dependencies = [ "nix", "serde", "serde_json", + "serde_with", "toml", "url", ] diff --git a/aziotctl/aziotctl-common/Cargo.toml b/aziotctl/aziotctl-common/Cargo.toml index 132e25b6a..32931ef16 100644 --- a/aziotctl/aziotctl-common/Cargo.toml +++ b/aziotctl/aziotctl-common/Cargo.toml @@ -12,6 +12,7 @@ log = "0.4" nix = "0.23" serde = { version = "1", features = ["derive"] } serde_json = "1.0.59" +serde_with = "1.11" url = { version = "2", features = ["serde"] } aziot-certd-config = { path = "../../cert/aziot-certd-config" } diff --git a/aziotctl/aziotctl-common/src/host_info.rs b/aziotctl/aziotctl-common/src/host_info.rs new file mode 100644 index 000000000..539811f89 --- /dev/null +++ b/aziotctl/aziotctl-common/src/host_info.rs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft. All rights reserved. + +use std::env::consts::ARCH; +use std::fs; +use std::io::{self, BufRead}; + +use serde::Serialize; +use serde_with::skip_serializing_none; + +#[cfg(target_pointer_width = "32")] +const BITNESS: usize = 32; +#[cfg(target_pointer_width = "64")] +const BITNESS: usize = 64; +#[cfg(target_pointer_width = "128")] +const BITNESS: usize = 128; + +/// A subset of the DMI variables exposed through /sys/devices/virtual/dmi/id. +/// +/// Examples: +/// +/// ```ignore +/// Host | name | vendor +/// ---------+-----------------+----------------------- +/// Hyper-V | Virtual Machine | Microsoft Corporation +/// ``` +/// +/// Ref: +#[derive(Clone, Debug, Serialize)] +pub struct DmiInfo { + pub product: Option, + pub vendor: Option, +} + +impl Default for DmiInfo { + fn default() -> Self { + Self { + product: try_read_dmi("product_name"), + vendor: try_read_dmi("sys_vendor"), + } + } +} + +/// A subset of the fields from /etc/os-release. +/// +/// Examples: +/// +/// ```ignore +/// OS | id | version_id +/// ---------------------+---------------------+------------ +/// CentOS 7 | centos | 7 +/// Debian 9 | debian | 9 +/// openSUSE Tumbleweed | opensuse-tumbleweed | 20190325 +/// Ubuntu 18.04 | ubuntu | 18.04 +/// ``` +/// +/// Ref: +#[skip_serializing_none] +#[derive(Clone, Debug, Serialize)] +pub struct OsInfo { + pub id: Option, + pub version_id: Option, + pub arch: &'static str, + pub bitness: usize, +} + +impl Default for OsInfo { + fn default() -> Self { + let mut result = Self { + id: None, + version_id: None, + arch: ARCH, + bitness: BITNESS, + }; + + let os_release = + fs::File::open("/etc/os-release").or_else(|_| fs::File::open("/usr/lib/os-release")); + + if let Ok(os_release) = os_release { + let os_release = io::BufReader::new(os_release); + + for line in os_release.lines() { + if let Ok(line) = &line { + match parse_shell_line(line) { + Some(("ID", value)) => { + result.id = Some(value.to_owned()); + } + Some(("VERSION_ID", value)) => { + result.version_id = Some(value.to_owned()); + } + _ => (), + }; + } else { + break; + } + } + } + + result + } +} + +pub fn parse_shell_line(line: &str) -> Option<(&str, &str)> { + let line = line.trim(); + + let (key, value) = line.split_once('=')?; + + // The value is essentially a shell string, so it can be quoted in single or + // double quotes, and can have escaped sequences using backslash. + // For simplicitly, just trim the quotes instead of implementing a full shell + // string grammar. + let value = if (value.starts_with('\'') && value.ends_with('\'')) + || (value.starts_with('"') && value.ends_with('"')) + { + &value[1..(value.len() - 1)] + } else { + value + }; + + Some((key, value)) +} + +fn try_read_dmi(entry: &'static str) -> Option { + let path = format!("/sys/devices/virtual/dmi/id/{}", entry); + + let bytes = fs::read(path).ok()?; + + Some(std::str::from_utf8(&bytes).ok()?.trim().to_owned()) +} diff --git a/aziotctl/aziotctl-common/src/lib.rs b/aziotctl/aziotctl-common/src/lib.rs index 520a7a157..ca02a9fe1 100644 --- a/aziotctl/aziotctl-common/src/lib.rs +++ b/aziotctl/aziotctl-common/src/lib.rs @@ -20,6 +20,7 @@ use serde::{Deserialize, Serialize}; pub mod check_last_modified; pub mod config; +pub mod host_info; pub mod system; #[derive(Debug, Serialize, Deserialize)] diff --git a/aziotctl/src/internal/check/additional_info.rs b/aziotctl/src/internal/check/additional_info.rs index 18d486e6f..489dca7de 100644 --- a/aziotctl/src/internal/check/additional_info.rs +++ b/aziotctl/src/internal/check/additional_info.rs @@ -1,18 +1,18 @@ // Copyright (c) Microsoft. All rights reserved. -use std::env::consts::ARCH; -use std::str; - use byte_unit::{Byte, ByteUnit}; use serde::Serialize; use sysinfo::{DiskExt, SystemExt}; +use aziotctl_common::host_info::{DmiInfo, OsInfo}; + /// Additional info for the JSON output of `aziotctl check` #[derive(Clone, Debug, Serialize)] pub struct AdditionalInfo { // TODO: update https://github.com/Azure/azure-iotedge to include aziotd version now: chrono::DateTime, os: OsInfo, + dmi: DmiInfo, system_info: SystemInfo, #[serde(skip_serializing_if = "Option::is_none")] @@ -26,7 +26,8 @@ impl AdditionalInfo { pub fn new(iothub_hostname: Option, local_gateway_hostname: Option) -> Self { AdditionalInfo { now: chrono::Utc::now(), - os: OsInfo::new(), + os: OsInfo::default(), + dmi: DmiInfo::default(), system_info: SystemInfo::new(), iothub_hostname, @@ -35,88 +36,6 @@ impl AdditionalInfo { } } -/// A subset of the fields from /etc/os-release. -/// -/// Examples: -/// -/// ```ignore -/// OS | id | version_id -/// ---------------------+---------------------+------------ -/// CentOS 7 | centos | 7 -/// Debian 9 | debian | 9 -/// openSUSE Tumbleweed | opensuse-tumbleweed | 20190325 -/// Ubuntu 18.04 | ubuntu | 18.04 -/// ``` -/// -/// Ref: -#[derive(Clone, Debug, Serialize)] -pub struct OsInfo { - id: Option, - version_id: Option, - arch: &'static str, - bitness: usize, -} - -impl OsInfo { - pub fn new() -> Self { - use std::fs::File; - use std::io::{BufRead, BufReader}; - - let mut result = OsInfo { - id: None, - version_id: None, - arch: ARCH, - // Technically wrong if someone runs an arm32 build on arm64, - // but we have dedicated arm64 builds so hopefully they don't. - bitness: std::mem::size_of::() * 8, - }; - - if let Ok(os_release) = File::open("/etc/os-release") { - let mut os_release = BufReader::new(os_release); - - let mut line = String::new(); - loop { - match os_release.read_line(&mut line) { - Ok(0) | Err(_) => break, - Ok(_) => { - if let Some((key, value)) = parse_os_release_line(&line) { - if key == "ID" { - result.id = Some(value.to_owned()); - } else if key == "VERSION_ID" { - result.version_id = Some(value.to_owned()); - } - } - - line.clear(); - } - } - } - } - - result - } -} - -fn parse_os_release_line(line: &str) -> Option<(&str, &str)> { - let line = line.trim(); - - let (key, value) = line.split_once('=')?; - - // The value is essentially a shell string, so it can be quoted in single or - // double quotes, and can have escaped sequences using backslash. - // For simplicitly, just trim the quotes instead of implementing a full shell - // string grammar. - let value = if (value.starts_with('\'') && value.ends_with('\'')) - || (value.starts_with('"') && value.ends_with('"')) - { - &value[1..(value.len() - 1)] - } else { - value - }; - - Some((key, value)) -} - #[derive(Clone, Debug, Default, Serialize)] struct SystemInfo { used_ram: String,