public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Aaron Lauterer <a.lauterer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH v1 installer 10/18] auto-installer: add utils
Date: Tue, 23 Jan 2024 18:00:45 +0100	[thread overview]
Message-ID: <20240123170053.490250-11-a.lauterer@proxmox.com> (raw)
In-Reply-To: <20240123170053.490250-1-a.lauterer@proxmox.com>

contains several utility structs and functions.

For example: a simple pattern matcher that matches wildcards at the
beginning or end of the filter.

It currently uses a dedicated function (parse_answer) to generate the
InstallConfig struct instead of a From implementation. This is because
for now the source data is spread over several other structs in
comparison to one in the TUI installer.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---
 proxmox-auto-installer/src/lib.rs   |   1 +
 proxmox-auto-installer/src/utils.rs | 473 ++++++++++++++++++++++++++++
 2 files changed, 474 insertions(+)
 create mode 100644 proxmox-auto-installer/src/utils.rs

diff --git a/proxmox-auto-installer/src/lib.rs b/proxmox-auto-installer/src/lib.rs
index 8cda416..72884c1 100644
--- a/proxmox-auto-installer/src/lib.rs
+++ b/proxmox-auto-installer/src/lib.rs
@@ -1,2 +1,3 @@
 pub mod answer;
 pub mod udevinfo;
+pub mod utils;
diff --git a/proxmox-auto-installer/src/utils.rs b/proxmox-auto-installer/src/utils.rs
new file mode 100644
index 0000000..6e650c5
--- /dev/null
+++ b/proxmox-auto-installer/src/utils.rs
@@ -0,0 +1,473 @@
+use anyhow::{anyhow, bail, Context, Result};
+use log::info;
+use std::{
+    collections::BTreeMap,
+    net::IpAddr,
+    process::{Command, Stdio},
+    str::FromStr,
+};
+
+use crate::{
+    answer,
+    answer::{Answer, FilterMatch},
+    udevinfo::UdevInfo,
+};
+use proxmox_installer_common::{
+    options::{
+        BtrfsRaidLevel, FsType, NetworkOptions, ZfsChecksumOption, ZfsCompressOption, ZfsRaidLevel,
+    },
+    setup::{InstallConfig, InstallZfsOption, LocaleInfo, RuntimeInfo, SetupInfo},
+    utils::{CidrAddress, Fqdn},
+};
+use serde::Deserialize;
+
+/// Supports the globbing character '*' at the beginning, end or both of the pattern.
+/// Globbing within the pattern is not supported
+fn find_with_glob(pattern: &str, value: &str) -> bool {
+    let globbing_symbol = '*';
+    let mut start_glob = false;
+    let mut end_glob = false;
+    let mut pattern = pattern;
+
+    if pattern.starts_with(globbing_symbol) {
+        start_glob = true;
+        pattern = &pattern[1..];
+    }
+
+    if pattern.ends_with(globbing_symbol) {
+        end_glob = true;
+        pattern = &pattern[..pattern.len() - 1]
+    }
+
+    match (start_glob, end_glob) {
+        (true, true) => value.contains(pattern),
+        (true, false) => value.ends_with(pattern),
+        (false, true) => value.starts_with(pattern),
+        _ => value == pattern,
+    }
+}
+
+pub fn get_network_settings(
+    answer: &Answer,
+    udev_info: &UdevInfo,
+    runtime_info: &RuntimeInfo,
+    setup_info: &SetupInfo,
+) -> Result<NetworkOptions> {
+    let mut network_options = NetworkOptions::defaults_from(setup_info, &runtime_info.network);
+
+    info!("Setting network configuration");
+
+    // Always use the FQDN from the answer file
+    network_options.fqdn = Fqdn::from(answer.global.fqdn.as_str())
+        .map_err(|err| anyhow!("Error parsing FQDN: {err}"))?;
+
+    if answer.network.use_dhcp.is_none() || !answer.network.use_dhcp.unwrap() {
+        network_options.address = CidrAddress::from_str(
+            answer
+                .network
+                .cidr
+                .clone()
+                .context("No CIDR defined")?
+                .as_str(),
+        )
+        .map_err(|_| anyhow!("Error parsing CIDR"))?;
+        network_options.dns_server = IpAddr::from_str(
+            answer
+                .network
+                .dns
+                .clone()
+                .context("No DNS server defined")?
+                .as_str(),
+        )
+        .map_err(|_| anyhow!("Error parsing DNS server"))?;
+        network_options.gateway = IpAddr::from_str(
+            answer
+                .network
+                .gateway
+                .clone()
+                .expect("No gateway defined")
+                .as_str(),
+        )
+        .map_err(|_| anyhow!("Error parsing gateway"))?;
+        network_options.ifname =
+            get_single_udev_index(answer.network.filter.clone().unwrap(), &udev_info.nics)?
+    }
+
+    Ok(network_options)
+}
+
+fn get_single_udev_index(
+    filter: BTreeMap<String, String>,
+    udev_list: &BTreeMap<String, BTreeMap<String, String>>,
+) -> Result<String> {
+    if filter.is_empty() {
+        bail!("no filter defined");
+    }
+    let mut dev_index: Option<String> = None;
+    'outer: for (dev, dev_values) in udev_list {
+        for (filter_key, filter_value) in &filter {
+            for (udev_key, udev_value) in dev_values {
+                if udev_key == filter_key && find_with_glob(filter_value, udev_value) {
+                    dev_index = Some(dev.clone());
+                    break 'outer; // take first match
+                }
+            }
+        }
+    }
+    if dev_index.is_none() {
+        bail!("filter did not match any device");
+    }
+
+    Ok(dev_index.unwrap())
+}
+
+fn get_matched_udev_indexes(
+    filter: BTreeMap<String, String>,
+    udev_list: &BTreeMap<String, BTreeMap<String, String>>,
+    match_all: bool,
+) -> Result<Vec<String>> {
+    let mut matches = vec![];
+    for (dev, dev_values) in udev_list {
+        let mut did_match_once = false;
+        let mut did_match_all = true;
+        for (filter_key, filter_value) in &filter {
+            for (udev_key, udev_value) in dev_values {
+                if udev_key == filter_key && find_with_glob(filter_value, udev_value) {
+                    did_match_once = true;
+                } else if udev_key == filter_key {
+                    did_match_all = false;
+                }
+            }
+        }
+        if (match_all && did_match_all) || (!match_all && did_match_once) {
+            matches.push(dev.clone());
+        }
+    }
+    if matches.is_empty() {
+        bail!("filter did not match any devices");
+    }
+    matches.sort();
+    Ok(matches)
+}
+
+pub fn set_disks(
+    answer: &Answer,
+    udev_info: &UdevInfo,
+    runtime_info: &RuntimeInfo,
+    config: &mut InstallConfig,
+) -> Result<()> {
+    match config.filesys {
+        FsType::Ext4 | FsType::Xfs => set_single_disk(answer, udev_info, runtime_info, config),
+        FsType::Zfs(_) | FsType::Btrfs(_) => {
+            set_selected_disks(answer, udev_info, runtime_info, config)
+        }
+    }
+}
+
+fn set_single_disk(
+    answer: &Answer,
+    udev_info: &UdevInfo,
+    runtime_info: &RuntimeInfo,
+    config: &mut InstallConfig,
+) -> Result<()> {
+    match &answer.disks.disk_selection {
+        Some(selection) => {
+            let disk_name = selection[0].clone();
+            let disk = runtime_info
+                .disks
+                .iter()
+                .find(|item| item.path.ends_with(disk_name.as_str()));
+            match disk {
+                Some(disk) => config.target_hd = Some(disk.clone()),
+                None => bail!("disk in 'disk_selection' not found"),
+            }
+        }
+        None => {
+            let disk_index =
+                get_single_udev_index(answer.disks.filter.clone().unwrap(), &udev_info.disks)?;
+            let disk = runtime_info
+                .disks
+                .iter()
+                .find(|item| item.index == disk_index);
+            config.target_hd = disk.cloned();
+        }
+    }
+    Ok(())
+}
+
+fn set_selected_disks(
+    answer: &Answer,
+    udev_info: &UdevInfo,
+    runtime_info: &RuntimeInfo,
+    config: &mut InstallConfig,
+) -> Result<()> {
+    match &answer.disks.disk_selection {
+        Some(selection) => {
+            info!("Disk selection found");
+            for disk_name in selection {
+                let disk = runtime_info
+                    .disks
+                    .iter()
+                    .find(|item| item.path.ends_with(disk_name.as_str()));
+                if let Some(disk) = disk {
+                    config
+                        .disk_selection
+                        .insert(disk.index.clone(), disk.index.clone());
+                }
+            }
+        }
+        None => {
+            info!("No disk selection found, looking for disk filters");
+            let filter_match = answer
+                .disks
+                .filter_match
+                .clone()
+                .unwrap_or(FilterMatch::Any);
+            let disk_filters = answer
+                .disks
+                .filter
+                .clone()
+                .context("no disk filters defined")?;
+            let selected_disk_indexes = get_matched_udev_indexes(
+                disk_filters,
+                &udev_info.disks,
+                filter_match == FilterMatch::All,
+            )?;
+
+            for i in selected_disk_indexes.into_iter() {
+                let disk = runtime_info
+                    .disks
+                    .iter()
+                    .find(|item| item.index == i)
+                    .unwrap();
+                config
+                    .disk_selection
+                    .insert(disk.index.clone(), disk.index.clone());
+            }
+        }
+    }
+    if config.disk_selection.is_empty() {
+        bail!("No disks found matching selection.");
+    }
+    Ok(())
+}
+
+pub fn get_first_selected_disk(config: &InstallConfig) -> usize {
+    config
+        .disk_selection
+        .iter()
+        .next()
+        .expect("no disks found")
+        .0
+        .parse::<usize>()
+        .expect("could not parse key to usize")
+}
+
+pub fn verify_locale_settings(answer: &Answer, locales: &LocaleInfo) -> Result<()> {
+    info!("Verifying locale settings");
+    if !locales
+        .countries
+        .keys()
+        .any(|i| i == &answer.global.country)
+    {
+        bail!("country code '{}' is not valid", &answer.global.country);
+    }
+    if !locales.kmap.keys().any(|i| i == &answer.global.keyboard) {
+        bail!("keyboard layout '{}' is not valid", &answer.global.keyboard);
+    }
+    if !locales
+        .cczones
+        .iter()
+        .any(|(_, zones)| zones.contains(&answer.global.timezone))
+    {
+        bail!("timezone '{}' is not valid", &answer.global.timezone);
+    }
+    Ok(())
+}
+
+pub fn run_cmds(step: &str, cmd_vec: &Option<Vec<String>>) -> Result<()> {
+    if let Some(cmds) = cmd_vec {
+        if !cmds.is_empty() {
+            info!("Running {step}-Commands:");
+            run_cmd(cmds)?;
+            info!("{step}-Commands finished");
+        }
+    }
+    Ok(())
+}
+
+fn run_cmd(cmds: &Vec<String>) -> Result<()> {
+    for cmd in cmds {
+        info!("Command '{cmd}':");
+        let mut child = match Command::new("/bin/bash")
+            .arg("-c")
+            .arg(cmd.clone())
+            .stdout(Stdio::inherit())
+            .stderr(Stdio::inherit())
+            .spawn()
+        {
+            Ok(child) => child,
+            Err(err) => bail!("error running command {cmd}: {err}"),
+        };
+        if let Err(err) = child.wait() {
+            bail!("{err}");
+        }
+    }
+
+    Ok(())
+}
+
+pub fn parse_answer(
+    answer: &Answer,
+    udev_info: &UdevInfo,
+    runtime_info: &RuntimeInfo,
+    locales: &LocaleInfo,
+    setup_info: &SetupInfo,
+) -> Result<InstallConfig> {
+    info!("Parsing answer file");
+    info!("Setting File system");
+    let filesystem = match &answer.disks.filesystem {
+        Some(answer::Filesystem::Ext4) => FsType::Ext4,
+        Some(answer::Filesystem::Xfs) => FsType::Xfs,
+        Some(answer::Filesystem::ZfsRaid0) => FsType::Zfs(ZfsRaidLevel::Raid0),
+        Some(answer::Filesystem::ZfsRaid1) => FsType::Zfs(ZfsRaidLevel::Raid1),
+        Some(answer::Filesystem::ZfsRaid10) => FsType::Zfs(ZfsRaidLevel::Raid10),
+        Some(answer::Filesystem::ZfsRaidZ1) => FsType::Zfs(ZfsRaidLevel::RaidZ),
+        Some(answer::Filesystem::ZfsRaidZ2) => FsType::Zfs(ZfsRaidLevel::RaidZ2),
+        Some(answer::Filesystem::ZfsRaidZ3) => FsType::Zfs(ZfsRaidLevel::RaidZ3),
+        Some(answer::Filesystem::BtrfsRaid0) => FsType::Btrfs(BtrfsRaidLevel::Raid0),
+        Some(answer::Filesystem::BtrfsRaid1) => FsType::Btrfs(BtrfsRaidLevel::Raid1),
+        Some(answer::Filesystem::BtrfsRaid10) => FsType::Btrfs(BtrfsRaidLevel::Raid10),
+        None => FsType::Ext4,
+    };
+    info!("File system selected: {}", filesystem);
+
+    let network_settings = get_network_settings(answer, udev_info, runtime_info, setup_info)?;
+
+    verify_locale_settings(answer, locales)?;
+
+    let mut config = InstallConfig {
+        autoreboot: 1_usize,
+        filesys: filesystem,
+        hdsize: 0.,
+        swapsize: None,
+        maxroot: None,
+        minfree: None,
+        maxvz: None,
+        zfs_opts: None,
+        target_hd: None,
+        disk_selection: BTreeMap::new(),
+
+        country: answer.global.country.clone(),
+        timezone: answer.global.timezone.clone(),
+        keymap: answer.global.keyboard.clone(),
+
+        password: answer.global.password.clone(),
+        mailto: answer.global.mailto.clone(),
+
+        mngmt_nic: network_settings.ifname,
+
+        hostname: network_settings.fqdn.host().unwrap().to_string(),
+        domain: network_settings.fqdn.domain(),
+        cidr: network_settings.address,
+        gateway: network_settings.gateway,
+        dns: network_settings.dns_server,
+    };
+
+    set_disks(answer, udev_info, runtime_info, &mut config)?;
+    match &config.filesys {
+        FsType::Xfs | FsType::Ext4 => {
+            let lvm = match &answer.disks.lvm {
+                Some(lvm) => lvm.clone(),
+                None => answer::LvmOptions::new(),
+            };
+            config.hdsize = lvm.hdsize.unwrap_or(config.target_hd.clone().unwrap().size);
+            config.swapsize = lvm.swapsize;
+            config.maxroot = lvm.maxroot;
+            config.maxvz = lvm.maxvz;
+            config.minfree = lvm.minfree;
+        }
+        FsType::Zfs(_) => {
+            let zfs = match &answer.disks.zfs {
+                Some(zfs) => zfs.clone(),
+                None => answer::ZfsOptions::new(),
+            };
+            let first_selected_disk = get_first_selected_disk(&config);
+
+            config.hdsize = zfs
+                .hdsize
+                .unwrap_or(runtime_info.disks[first_selected_disk].size);
+            config.zfs_opts = Some(InstallZfsOption {
+                ashift: zfs.ashift.unwrap_or(12),
+                arc_max: zfs.arc_max.unwrap_or(2048),
+                compress: match zfs.compress {
+                    Some(answer::ZfsCompressOption::On) => ZfsCompressOption::On,
+                    Some(answer::ZfsCompressOption::Off) => ZfsCompressOption::Off,
+                    Some(answer::ZfsCompressOption::Lzjb) => ZfsCompressOption::Lzjb,
+                    Some(answer::ZfsCompressOption::Lz4) => ZfsCompressOption::Lz4,
+                    Some(answer::ZfsCompressOption::Zle) => ZfsCompressOption::Zle,
+                    Some(answer::ZfsCompressOption::Gzip) => ZfsCompressOption::Gzip,
+                    Some(answer::ZfsCompressOption::Zstd) => ZfsCompressOption::Zstd,
+                    None => ZfsCompressOption::On,
+                },
+                checksum: match zfs.checksum {
+                    Some(answer::ZfsChecksumOption::On) => ZfsChecksumOption::On,
+                    Some(answer::ZfsChecksumOption::Off) => ZfsChecksumOption::Off,
+                    Some(answer::ZfsChecksumOption::Fletcher2) => ZfsChecksumOption::Fletcher2,
+                    Some(answer::ZfsChecksumOption::Fletcher4) => ZfsChecksumOption::Fletcher4,
+                    Some(answer::ZfsChecksumOption::Sha256) => ZfsChecksumOption::Sha256,
+                    None => ZfsChecksumOption::On,
+                },
+                copies: zfs.copies.unwrap_or(1),
+            });
+        }
+        FsType::Btrfs(_) => {
+            let btrfs = match &answer.disks.btrfs {
+                Some(btrfs) => btrfs.clone(),
+                None => answer::BtrfsOptions::new(),
+            };
+            let first_selected_disk = get_first_selected_disk(&config);
+
+            config.hdsize = btrfs
+                .hdsize
+                .unwrap_or(runtime_info.disks[first_selected_disk].size);
+        }
+    }
+    Ok(config)
+}
+
+#[derive(Clone, Debug, Deserialize, PartialEq)]
+#[serde(tag = "type", rename_all = "lowercase")]
+pub enum LowLevelMessage {
+    #[serde(rename = "message")]
+    Info {
+        message: String,
+    },
+    Error {
+        message: String,
+    },
+    Prompt {
+        query: String,
+    },
+    Finished {
+        state: String,
+        message: String,
+    },
+    Progress {
+        ratio: f32,
+        text: String,
+    },
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    #[test]
+    fn test_glob_patterns() {
+        let test_value = "foobar";
+        assert_eq!(find_with_glob("*bar", test_value), true);
+        assert_eq!(find_with_glob("foo*", test_value), true);
+        assert_eq!(find_with_glob("foobar", test_value), true);
+        assert_eq!(find_with_glob("oobar", test_value), false);
+    }
+}
-- 
2.39.2





  parent reply	other threads:[~2024-01-23 17:01 UTC|newest]

Thread overview: 32+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2024-01-23 17:00 [pve-devel] [PATCH v1 installer/docs 00/18] add automated/unattended installation Aaron Lauterer
2024-01-23 17:00 ` [pve-devel] [PATCH v1 installer 01/18] tui: common: move InstallConfig struct to common crate Aaron Lauterer
2024-01-23 17:00 ` [pve-devel] [PATCH v1 installer 02/18] common: make InstallZfsOption members public Aaron Lauterer
2024-01-23 17:00 ` [pve-devel] [PATCH v1 installer 03/18] common: tui: use BTreeMap for predictable ordering Aaron Lauterer
2024-01-23 17:00 ` [pve-devel] [PATCH v1 installer 04/18] Makefile: fix handling of multiple usr_bin files Aaron Lauterer
2024-02-06 14:28   ` [pve-devel] applied: " Thomas Lamprecht
2024-01-23 17:00 ` [pve-devel] [PATCH v1 installer 05/18] low-level: add dump-udev command Aaron Lauterer
2024-01-23 17:00 ` [pve-devel] [PATCH v1 installer 06/18] add auto-installer crate Aaron Lauterer
2024-01-23 17:00 ` [pve-devel] [PATCH v1 installer 07/18] auto-installer: add dependencies Aaron Lauterer
2024-01-31 13:52   ` Christoph Heiss
2024-01-23 17:00 ` [pve-devel] [PATCH v1 installer 08/18] auto-installer: add answer file definition Aaron Lauterer
2024-01-31 13:50   ` Christoph Heiss
2024-02-23 14:27   ` Stefan Lendl
2024-02-27 13:45     ` Aaron Lauterer
2024-01-23 17:00 ` [pve-devel] [PATCH v1 installer 09/18] auto-installer: add struct to hold udev info Aaron Lauterer
2024-01-23 17:00 ` Aaron Lauterer [this message]
2024-01-23 17:00 ` [pve-devel] [PATCH v1 installer 11/18] auto-installer: add simple logging Aaron Lauterer
2024-01-23 17:00 ` [pve-devel] [PATCH v1 installer 12/18] auto-installer: add tests for answer file parsing Aaron Lauterer
2024-01-23 17:00 ` [pve-devel] [PATCH v1 installer 13/18] auto-installer: add auto-installer binary Aaron Lauterer
2024-01-23 17:00 ` [pve-devel] [PATCH v1 installer 14/18] auto-installer: add fetch answer binary Aaron Lauterer
2024-02-06 11:33   ` Christoph Heiss
2024-02-08 14:18   ` Christoph Heiss
2024-02-08 16:46     ` Aaron Lauterer
2024-02-16 16:34       ` Aaron Lauterer
2024-01-23 17:00 ` [pve-devel] [PATCH v1 installer 15/18] auto-installer: use glob crate for pattern matching Aaron Lauterer
2024-02-08  9:01   ` Christoph Heiss
2024-01-23 17:00 ` [pve-devel] [PATCH v1 installer 16/18] auto-installer: utils: make get_udev_index functions public Aaron Lauterer
2024-01-23 17:00 ` [pve-devel] [PATCH v1 installer 17/18] auto-installer: add proxmox-installer-filter helper tool Aaron Lauterer
2024-01-23 17:00 ` [pve-devel] [PATCH v1 docs 18/18] installation: add unattended documentation Aaron Lauterer
2024-02-08 10:26 ` [pve-devel] [PATCH v1 installer/docs 00/18] add automated/unattended installation Christoph Heiss
2024-02-08 10:34   ` Christoph Heiss
2024-02-08 11:32     ` Aaron Lauterer

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20240123170053.490250-11-a.lauterer@proxmox.com \
    --to=a.lauterer@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal