[pve-devel] [PATCH v2 18/22] auto-installer: fetch: add gathering of system identifiers and restructure code

Aaron Lauterer a.lauterer at proxmox.com
Wed Feb 21 12:08:01 CET 2024


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 <a.lauterer at proxmox.com>
---
 .../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<PathBuf> {
-    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<PathBuf> {
+    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<String> {
+    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<bool> {
+    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<Vec<String>> {
+    let ip_output = Command::new("/usr/sbin/ip")
+        .arg("-j")
+        .arg("link")
+        .output()?;
+    let parsed_links: Vec<IpLinksUdevInfo> =
+        serde_json::from_str(String::from_utf8(ip_output.stdout)?.as_str())?;
+    let mut links: Vec<String> = 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<String> {
+    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<String> = 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<String, String>,
+    baseboard: HashMap<String, String>,
+    chassis: HashMap<String, String>,
+    mac_addresses: Vec<String>,
+}
+
+fn parse_dmidecode(output: &str) -> Result<HashMap<String, String>> {
+    let keywords = vec![
+        "Asset Tag",
+        "Product Name",
+        "Serial Number",
+        "SKU Number",
+        "UUID",
+    ];
+
+    let mut res: HashMap<String, String> = 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<String, String> = 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<String, String> = 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<String, String> = 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





More information about the pve-devel mailing list