use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
use std::env::consts::ARCH;
use std::path::PathBuf;
use crate::{
collectors::{
hardware_info::{table_load_from_device, SystemInfo},
os_info::{to_debian_architecture, CommandRunner, SystemCommandRunner},
},
constants,
models::{
devices::{Bios, Board, Chassis, PCIPeripheral, Processor, USBPeripheral},
software::OS,
},
};
#[cfg(target_arch = "x86_64")]
use smbioslib::{
SMBiosBaseboardInformation, SMBiosInformation, SMBiosProcessorInformation,
SMBiosSystemChassisInformation, SMBiosSystemInformation,
};
#[cfg(not(target_arch = "x86_64"))]
use crate::collectors::cpuinfo::CpuInfo;
#[derive(Debug, Clone)]
pub struct Paths {
pub smbios_entry_filepath: PathBuf,
pub smbios_table_filepath: PathBuf,
pub cpuinfo_filepath: PathBuf,
pub max_cpu_frequency_filepath: PathBuf,
pub device_tree_dirpath: PathBuf,
pub os_release_filepath: PathBuf,
pub proc_version_filepath: PathBuf,
}
impl Default for Paths {
fn default() -> Self {
let smbios_entry_filepath = PathBuf::from(smbioslib::SYS_ENTRY_FILE);
let smbios_table_filepath = PathBuf::from(smbioslib::SYS_TABLE_FILE);
let cpuinfo_filepath = PathBuf::from(constants::PROC_CPUINFO_FILE_PATH);
let max_cpu_frequency_filepath = PathBuf::from(constants::CPU_MAX_FREQ_FILE_PATH);
let device_tree_dirpath = PathBuf::from(constants::PROC_DEVICE_TREE_DIR_PATH);
let os_release_filepath = PathBuf::from(constants::OS_RELEASE_FILE_PATH);
let proc_version_filepath = PathBuf::from(constants::PROC_VERSION_FILE_PATH);
Self {
smbios_entry_filepath,
smbios_table_filepath,
cpuinfo_filepath,
max_cpu_frequency_filepath,
device_tree_dirpath,
os_release_filepath,
proc_version_filepath,
}
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct CertificationStatusRequest {
pub architecture: String,
pub bios: Option<Bios>,
pub board: Board,
pub chassis: Option<Chassis>,
pub model: String,
pub os: OS,
pub pci_peripherals: Vec<PCIPeripheral>,
pub processor: Processor,
pub usb_peripherals: Vec<USBPeripheral>,
pub vendor: String,
}
impl CertificationStatusRequest {
pub fn new(paths: Paths) -> Result<Self> {
Self::new_with_runner(paths, &SystemCommandRunner)
}
pub(crate) fn new_with_runner(paths: Paths, runner: &impl CommandRunner) -> Result<Self> {
Self::from(paths, runner)
}
#[cfg(target_arch = "x86_64")]
fn from(paths: Paths, runner: &impl CommandRunner) -> Result<Self> {
let Paths {
smbios_entry_filepath,
smbios_table_filepath,
max_cpu_frequency_filepath,
os_release_filepath,
proc_version_filepath,
..
} = paths;
let data = table_load_from_device(&smbios_entry_filepath, &smbios_table_filepath)?;
let bios_info_vec = data.collect::<SMBiosInformation>();
let bios_info = bios_info_vec
.first()
.ok_or_else(|| anyhow!("failed to load BIOS data"))?;
let bios = Some(Bios::try_from(bios_info)?);
let processor_info_vec = data.collect::<SMBiosProcessorInformation>();
let processor_info = processor_info_vec
.first()
.ok_or_else(|| anyhow!("failed to load processor data"))?;
let processor =
Processor::try_from((processor_info, max_cpu_frequency_filepath.as_path()))?;
let chassis_info_vec = data.collect::<SMBiosSystemChassisInformation>();
let chassis = chassis_info_vec
.first()
.and_then(|info| Chassis::try_from(info).ok());
let board_info_vec = data.collect::<SMBiosBaseboardInformation>();
let board = board_info_vec
.first()
.map(Board::try_from)
.transpose()?
.unwrap_or_else(Board::default);
let system_data_vec = data.collect::<SMBiosSystemInformation>();
let system_data = system_data_vec.first().unwrap();
let system_info = SystemInfo::try_from_smbios(system_data)?;
let model = system_info.product_name;
let vendor = system_info.manufacturer;
let architecture = to_debian_architecture(ARCH)
.with_context(|| format!("cannot parse architecture {ARCH:?}"))?
.to_owned();
let os = OS::try_new(
os_release_filepath.as_path(),
proc_version_filepath.as_path(),
runner,
)
.context("cannot read OS release information")?;
let pci_peripherals = Vec::new();
let usb_peripherals = Vec::new();
Ok(Self {
architecture,
bios,
board,
chassis,
model,
os,
pci_peripherals,
processor,
usb_peripherals,
vendor,
})
}
#[cfg(not(target_arch = "x86_64"))]
fn from(paths: Paths, runner: &impl CommandRunner) -> Result<Self> {
let Paths {
cpuinfo_filepath,
max_cpu_frequency_filepath,
device_tree_dirpath,
os_release_filepath,
proc_version_filepath,
..
} = paths;
let cpu_info = CpuInfo::from_file(&cpuinfo_filepath.clone())?;
let architecture = to_debian_architecture(ARCH)
.with_context(|| format!("cannot parse architecture {ARCH:?}"))?
.to_owned();
let bios = None;
let board = Board::try_from(device_tree_dirpath.as_path())?;
let chassis = None;
let model = cpu_info.model;
let os = OS::try_new(
os_release_filepath.as_path(),
proc_version_filepath.as_path(),
runner,
)?;
let pci_peripherals = Vec::new();
let processor = Processor::try_from((
cpuinfo_filepath.as_path(),
max_cpu_frequency_filepath.as_path(),
))?;
let usb_peripherals = Vec::new();
let vendor = String::from("Unknown");
Ok(Self {
architecture,
bios,
board,
chassis,
model,
os,
pci_peripherals,
processor,
usb_peripherals,
vendor,
})
}
}
#[cfg(test)]
mod tests {
use crate::{
constants,
helpers::test_utils::{apply_vars, get_test_filepath, MockCommandRunner},
models::request_validators::{CertificationStatusRequest, Paths},
};
use serde_json::Value;
use simple_test_case::test_case;
use std::{fs::read_to_string, path::PathBuf};
#[test_case(
"amd64/dgx_station",
"jammy",
"22.04",
"5.4.0-192-generic",
&["nvme", "intel_lpss_pci", "intel_ish_ipc", "idma64"];
"jammy_dgx_station"
)]
#[test_case(
"amd64/dell_xps13",
"noble",
"24.04",
"6.8.0-1013-oem",
&["zfs", "spl", "nvme_tcp"];
"noble_dell_xps13"
)]
#[test_case(
"amd64/thinkstation_p620",
"focal",
"20.04",
"5.15.0-125-generic",
&["xt_tcpudp", "nft_chain_nat"];
"focal_thinkstation"
)]
#[test]
fn test_smbios_certification_request(
dir_path: &str,
codename: &str,
release: &str,
kernel_version: &str,
kernel_modules: &[&str],
) {
let paths = Paths {
smbios_entry_filepath: get_test_filepath(
format!("{dir_path}/smbios_entry_point").as_str(),
),
smbios_table_filepath: get_test_filepath(format!("{dir_path}/DMI").as_str()),
max_cpu_frequency_filepath: get_test_filepath(
format!("{dir_path}/cpuinfo_max_freq").as_str(),
),
os_release_filepath: get_test_filepath(format!("{dir_path}/os-release").as_str()),
proc_version_filepath: get_test_filepath(format!("{dir_path}/version").as_str()),
cpuinfo_filepath: PathBuf::from("./none"),
device_tree_dirpath: PathBuf::from("./none"),
};
let lsmod_output: String = std::iter::once("Module Size Used by\n".to_owned())
.chain(
kernel_modules
.iter()
.map(|module| format!("{module} 456092 0\n")),
)
.collect::<String>();
let mock_calls = vec![((constants::LSMOD, Vec::new()), Ok(lsmod_output.as_str()))];
let mock_runner = MockCommandRunner::new(mock_calls);
let quoted_kernel_modules: Vec<_> = kernel_modules
.iter()
.map(|module| format!("\"{module}\""))
.collect();
let kernel_modules_str = format!("[{}]", quoted_kernel_modules.join(", "));
let content = read_to_string(get_test_filepath(
format!("{dir_path}/request.json").as_str(),
))
.unwrap();
let expected_result = apply_vars(
content,
&[
("CODENAME", codename),
("KERNEL_VERSION", kernel_version),
("KERNEL_MODULES", kernel_modules_str.as_str()),
("RELEASE", release),
],
);
let cert_status_request_json = serde_json::to_value(
CertificationStatusRequest::new_with_runner(paths, &mock_runner).unwrap(),
)
.unwrap();
let expected_json: Value =
serde_json::from_str(expected_result.as_str()).expect("JSON was not well formatted");
assert_eq!(cert_status_request_json, expected_json);
}
}