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
next prev 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