From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id A5AB793E98 for ; Wed, 21 Feb 2024 12:08:48 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id C4313157FD for ; Wed, 21 Feb 2024 12:08:17 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Wed, 21 Feb 2024 12:08:12 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id D3F3B4446B for ; Wed, 21 Feb 2024 12:08:11 +0100 (CET) From: Aaron Lauterer To: pve-devel@lists.proxmox.com Date: Wed, 21 Feb 2024 12:08:01 +0100 Message-Id: <20240221110805.931925-19-a.lauterer@proxmox.com> X-Mailer: git-send-email 2.39.2 In-Reply-To: <20240221110805.931925-1-a.lauterer@proxmox.com> References: <20240221110805.931925-1-a.lauterer@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.188 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_LOTSOFHASH 0.25 Emails with lots of hash-like gibberish SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record T_SCC_BODY_TEXT_LINE -0.01 - Subject: [pve-devel] [PATCH v2 18/22] auto-installer: fetch: add gathering of system identifiers and restructure code X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Wed, 21 Feb 2024 11:08:48 -0000 They will be used as payload when POSTing a request for an answer file. The idea is, that with this information, it should be possible to identify the system and generate a matching answer file on the fly. Many of these properties can also be found on the machine or packaging of the machine and could therefore be scanned into a database. Identifiers are the following properties from `dmidecode` sections 1, 2, and 3: * Asset Tag * Product Name * Serial Number * SKU Number * UUID As well as a list of the MAC addresses of all the NICs. Since we now have more than a simple utils.rs module in the fetch plugins, it, and the additional fetch plugin utilities are placed in their own directory. Signed-off-by: Aaron Lauterer --- .../src/fetch_plugins/mod.rs | 2 +- .../src/fetch_plugins/utils.rs | 90 -------- .../src/fetch_plugins/utils/mod.rs | 113 ++++++++++ .../src/fetch_plugins/utils/sysinfo.rs | 200 ++++++++++++++++++ 4 files changed, 314 insertions(+), 91 deletions(-) delete mode 100644 proxmox-auto-installer/src/fetch_plugins/utils.rs create mode 100644 proxmox-auto-installer/src/fetch_plugins/utils/mod.rs create mode 100644 proxmox-auto-installer/src/fetch_plugins/utils/sysinfo.rs diff --git a/proxmox-auto-installer/src/fetch_plugins/mod.rs b/proxmox-auto-installer/src/fetch_plugins/mod.rs index 11d6937..6f1e8a2 100644 --- a/proxmox-auto-installer/src/fetch_plugins/mod.rs +++ b/proxmox-auto-installer/src/fetch_plugins/mod.rs @@ -1,2 +1,2 @@ pub mod partition; -mod utils; +pub mod utils; diff --git a/proxmox-auto-installer/src/fetch_plugins/utils.rs b/proxmox-auto-installer/src/fetch_plugins/utils.rs deleted file mode 100644 index 82cd3e0..0000000 --- a/proxmox-auto-installer/src/fetch_plugins/utils.rs +++ /dev/null @@ -1,90 +0,0 @@ -use anyhow::{bail, Result}; -use log::{info, warn}; -use std::{ - fs::create_dir_all, - path::{Path, PathBuf}, - process::Command, -}; - -/// Searches for upper and lower case existence of the partlabel in the search_path -/// -/// # Arguemnts -/// * `partlabel_lower` - Partition Label in lower case -/// * `search_path` - Path where to search for the partiiton label -/// search_path: String -pub fn scan_partlabels(partlabel_lower: &str, search_path: &str) -> Result { - let partlabel = partlabel_lower.to_uppercase(); - let path = Path::new(search_path).join(partlabel.clone()); - match path.try_exists() { - Ok(true) => { - info!("Found partition with label '{}'", partlabel); - return Ok(path); - } - Ok(false) => info!("Did not detect partition with label '{}'", partlabel), - Err(err) => info!("Encountered issue, accessing '{}': {}", path.display(), err), - } - - let partlabel = partlabel_lower.to_lowercase(); - let path = Path::new(search_path).join(partlabel.clone()); - match path.try_exists() { - Ok(true) => { - info!("Found partition with label '{}'", partlabel); - return Ok(path); - } - Ok(false) => info!("Did not detect partition with label '{}'", partlabel), - Err(err) => info!("Encountered issue, accessing '{}': {}", path.display(), err), - } - bail!( - "Could not detect upper or lower case labels for '{}'", - partlabel_lower - ); -} - -/// Will mount source path to target_path -/// -/// # Arguments -/// * `source` - `PathBuf` of the source location -/// * `target_path` - Location where to mount, will be created -pub fn mount_part(source: PathBuf, target_path: &str) -> Result<()> { - info!("Mounting partition at {target_path}"); - // create dir for mountpoint - create_dir_all(target_path)?; - match Command::new("mount") - .args(["-o", "ro"]) - .arg(source) - .arg(target_path) - .output() - { - Ok(output) => { - if output.status.success() { - Ok(()) - } else { - warn!("Error mounting: {}", String::from_utf8(output.stderr)?); - Ok(()) - } - } - Err(err) => bail!("Error mounting: {}", err), - } -} - -/// Tries to unmount the specified path. Will warn on errors, but not fail. -/// -/// # Arguemnts -/// * `target_path` - path to unmount -pub fn umount_part(target_path: &str) -> Result<()> { - info!("Unmounting partitiona at {target_path}"); - match Command::new("umount").arg(target_path).output() { - Ok(output) => { - if output.status.success() { - Ok(()) - } else { - warn!("Error unmounting: {}", String::from_utf8(output.stderr)?); - Ok(()) - } - } - Err(err) => { - warn!("Error unmounting: {}", err); - Ok(()) - } - } -} diff --git a/proxmox-auto-installer/src/fetch_plugins/utils/mod.rs b/proxmox-auto-installer/src/fetch_plugins/utils/mod.rs new file mode 100644 index 0000000..37341ee --- /dev/null +++ b/proxmox-auto-installer/src/fetch_plugins/utils/mod.rs @@ -0,0 +1,113 @@ +use anyhow::{Error, Result}; +use log::{info, warn}; +use serde::Deserialize; +use serde_json; +use std::{ + fs::{self, create_dir_all}, + path::{Path, PathBuf}, + process::Command, +}; + +static ANSWER_MP: &str = "/mnt/answer"; +static PARTLABEL: &str = "proxmoxinst"; +static SEARCH_PATH: &str = "/dev/disk/by-label"; + +pub mod sysinfo; + +/// Searches for upper and lower case existence of the partlabel in the search_path +/// +/// # Arguemnts +/// * `partlabel_lower` - Partition Label in lower case +/// * `search_path` - Path where to search for the partiiton label +/// search_path: String +pub fn scan_partlabels(partlabel_lower: &str, search_path: &str) -> Result { + let partlabel = partlabel_lower.to_uppercase(); + let path = Path::new(search_path).join(partlabel.clone()); + match path.try_exists() { + Ok(true) => { + info!("Found partition with label '{}'", partlabel); + return Ok(path); + } + Ok(false) => info!("Did not detect partition with label '{}'", partlabel), + Err(err) => info!("Encountered issue, accessing '{}': {}", path.display(), err), + } + + let partlabel = partlabel_lower.to_lowercase(); + let path = Path::new(search_path).join(partlabel.clone()); + match path.try_exists() { + Ok(true) => { + info!("Found partition with label '{}'", partlabel); + return Ok(path); + } + Ok(false) => info!("Did not detect partition with label '{}'", partlabel), + Err(err) => info!("Encountered issue, accessing '{}': {}", path.display(), err), + } + Err(Error::msg(format!( + "Could not detect upper or lower case labels for '{partlabel_lower}'" + ))) +} + +/// Will search and mount a partition/FS labeled proxmoxinst in lower or uppercase to ANSWER_MP; +pub fn mount_proxmoxinst_part() -> Result { + if let Ok(true) = check_if_mounted(ANSWER_MP) { + info!("Skipping: '{ANSWER_MP}' is already mounted."); + return Ok(ANSWER_MP.into()); + } + let part_path = scan_partlabels(PARTLABEL, SEARCH_PATH)?; + info!("Mounting partition at {ANSWER_MP}"); + // create dir for mountpoint + create_dir_all(ANSWER_MP)?; + match Command::new("mount") + .args(["-o", "ro"]) + .arg(part_path) + .arg(ANSWER_MP) + .output() + { + Ok(output) => { + if output.status.success() { + Ok(ANSWER_MP.into()) + } else { + warn!("Error mounting: {}", String::from_utf8(output.stderr)?); + Ok(ANSWER_MP.into()) + } + } + Err(err) => Err(Error::msg(format!("Error mounting: {err}"))), + } +} + +fn check_if_mounted(target_path: &str) -> Result { + let mounts = fs::read_to_string("/proc/mounts")?; + for line in mounts.lines() { + if let Some(mp) = line.split(' ').nth(1) { + if mp == target_path { + return Ok(true); + } + } + } + Ok(false) +} + +#[derive(Deserialize, Debug)] +struct IpLinksUdevInfo { + ifname: String, +} + +/// Returns vec of usable NICs +pub fn get_nic_list() -> Result> { + let ip_output = Command::new("/usr/sbin/ip") + .arg("-j") + .arg("link") + .output()?; + let parsed_links: Vec = + serde_json::from_str(String::from_utf8(ip_output.stdout)?.as_str())?; + let mut links: Vec = Vec::new(); + + for link in parsed_links { + if link.ifname == *"lo" { + continue; + } + links.push(link.ifname); + } + + Ok(links) +} diff --git a/proxmox-auto-installer/src/fetch_plugins/utils/sysinfo.rs b/proxmox-auto-installer/src/fetch_plugins/utils/sysinfo.rs new file mode 100644 index 0000000..74701cd --- /dev/null +++ b/proxmox-auto-installer/src/fetch_plugins/utils/sysinfo.rs @@ -0,0 +1,200 @@ +use anyhow::{bail, Result}; +use serde::Serialize; +use std::{collections::HashMap, fs, process::Command}; + +use super::get_nic_list; + +pub fn get_sysinfo(pretty: bool) -> Result { + let mut system = HashMap::new(); + let mut baseboard = HashMap::new(); + let mut chassis = HashMap::new(); + for option in 1..=3 { + let dmiresult = Command::new("dmidecode") + .arg("-t") + .arg(format!("{option}")) + .output()?; + + if dmiresult.status.success() { + let output = String::from_utf8(dmiresult.stdout)?; + match option { + 1 => system = parse_dmidecode(&output)?, + 2 => baseboard = parse_dmidecode(&output)?, + 3 => chassis = parse_dmidecode(&output)?, + _ => (), + } + } else { + let stderr = String::from_utf8(dmiresult.stderr)?; + bail!("Failed to get dmidecode information. Are you running as root? '{stderr}'"); + } + } + + let mut mac_addresses: Vec = Vec::new(); + let links = get_nic_list()?; + for link in links { + let address = fs::read_to_string(format!("/sys/class/net/{link}/address"))?; + let address = String::from(address.trim()); + mac_addresses.push(address); + } + + let sysinfo = SysInfo { + system, + baseboard, + chassis, + mac_addresses, + }; + if pretty { + return Ok(serde_json::to_string_pretty(&sysinfo)?); + } + Ok(serde_json::to_string(&sysinfo)?) +} + +#[derive(Debug, Serialize)] +struct SysInfo { + system: HashMap, + baseboard: HashMap, + chassis: HashMap, + mac_addresses: Vec, +} + +fn parse_dmidecode(output: &str) -> Result> { + let keywords = vec![ + "Asset Tag", + "Product Name", + "Serial Number", + "SKU Number", + "UUID", + ]; + + let mut res: HashMap = HashMap::new(); + for mut line in output.lines() { + line = line.trim(); + if let Some((key, value)) = line.split_once(':') { + if keywords.contains(&key) { + res.insert(String::from(key), String::from(value.trim())); + } + } + } + + Ok(res) +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::parse_dmidecode; + + #[test] + fn dmidecode_parse() { + let system1 = String::from( + r#" +# dmidecode 3.4 +Getting SMBIOS data from sysfs. +SMBIOS 3.2.0 present. + +Handle 0x0001, DMI type 1, 27 bytes +System Information + Manufacturer: GIGABYTE + Product Name: MZ32-AR0-00 + Version: 0100 + Serial Number: 01234567890123456789AB + UUID: 61df0000-9855-11ed-8000-b42e99acXXXX + Wake-up Type: Power Switch + SKU Number: 01234567890123456789AB + Family: Server"#, + ); + + let mut system1_check: HashMap = HashMap::new(); + system1_check.insert( + String::from("Serial Number"), + String::from("01234567890123456789AB"), + ); + system1_check.insert( + String::from("UUID"), + String::from("61df0000-9855-11ed-8000-b42e99acXXXX"), + ); + system1_check.insert( + String::from("SKU Number"), + String::from("01234567890123456789AB"), + ); + system1_check.insert( + String::from("Product Name"), + String::from("MZ32-AR0-00"), + ); + + let baseboard1 = String::from( + r#" +# dmidecode 3.4 +Getting SMBIOS data from sysfs. +SMBIOS 3.2.0 present. + +Handle 0x0002, DMI type 2, 15 bytes +Base Board Information + Manufacturer: GIGABYTE + Product Name: MZ32-AR0-00 + Version: 01000100 + Serial Number: JGBNA600XXX + Asset Tag: 01234567890123456789AB + Features: + Board is a hosting board + Board is removable + Board is replaceable + Location In Chassis: 01234567890123456789AB + Chassis Handle: 0x0003 + Type: Motherboard + Contained Object Handles: 0"#, + ); + let mut baseboard1_check: HashMap = HashMap::new(); + baseboard1_check.insert(String::from("Serial Number"), String::from("JGBNA600XXX")); + baseboard1_check.insert( + String::from("Asset Tag"), + String::from("01234567890123456789AB"), + ); + baseboard1_check.insert( + String::from("Product Name"), + String::from("MZ32-AR0-00"), + ); + + let chassis1 = String::from( + r#" +# dmidecode 3.4 +Getting SMBIOS data from sysfs. +SMBIOS 3.2.0 present. + +Handle 0x0003, DMI type 3, 22 bytes +Chassis Information + Manufacturer: GIGABYTE + Type: Main Server Chassis + Lock: Not Present + Version: 01234567 + Serial Number: 01234567890123456789AB + Asset Tag: 01234567890123456789AB + Boot-up State: Safe + Power Supply State: Safe + Thermal State: Safe + Security Status: None + OEM Information: 0x00000000 + Height: Unspecified + Number Of Power Cords: 1 + Contained Elements: 0 + SKU Number: 01234567890123456789AB"#, + ); + let mut chassis1_check: HashMap = HashMap::new(); + chassis1_check.insert( + String::from("Serial Number"), + String::from("01234567890123456789AB"), + ); + chassis1_check.insert( + String::from("Asset Tag"), + String::from("01234567890123456789AB"), + ); + chassis1_check.insert( + String::from("SKU Number"), + String::from("01234567890123456789AB"), + ); + + assert_eq!(parse_dmidecode(&system1).unwrap(), system1_check); + assert_eq!(parse_dmidecode(&baseboard1).unwrap(), baseboard1_check); + assert_eq!(parse_dmidecode(&chassis1).unwrap(), chassis1_check); + } +} -- 2.39.2