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 173C6BC4BA for ; Thu, 28 Mar 2024 14:50:43 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 4F9CACC12 for ; Thu, 28 Mar 2024 14:50:40 +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 ; Thu, 28 Mar 2024 14:50:35 +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 3FF144292B for ; Thu, 28 Mar 2024 14:50:35 +0100 (CET) From: Aaron Lauterer To: pve-devel@lists.proxmox.com Date: Thu, 28 Mar 2024 14:50:09 +0100 Message-Id: <20240328135028.504520-12-a.lauterer@proxmox.com> X-Mailer: git-send-email 2.39.2 In-Reply-To: <20240328135028.504520-1-a.lauterer@proxmox.com> References: <20240328135028.504520-1-a.lauterer@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.061 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 SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pve-devel] [PATCH v3 11/30] auto-installer: add utils 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: Thu, 28 Mar 2024 13:50:43 -0000 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 --- proxmox-auto-installer/src/lib.rs | 1 + proxmox-auto-installer/src/utils.rs | 417 ++++++++++++++++++++++++++++ 2 files changed, 418 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..bfd1743 --- /dev/null +++ b/proxmox-auto-installer/src/utils.rs @@ -0,0 +1,417 @@ +use anyhow::{bail, Result}; +use log::info; +use std::{ + collections::BTreeMap, + process::{Command, Stdio}, +}; + +use crate::{ + answer::{self, Answer}, + udevinfo::UdevInfo, +}; +use proxmox_installer_common::{ + options::{FsType, NetworkOptions, ZfsChecksumOption, ZfsCompressOption}, + setup::{InstallConfig, InstallZfsOption, LocaleInfo, RuntimeInfo, SetupInfo}, +}; +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 { + 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 = answer.global.fqdn.clone(); + + if let answer::NetworkSettings::Manual(settings) = &answer.network.network_settings { + network_options.address = settings.cidr.clone(); + network_options.dns_server = settings.dns; + network_options.gateway = settings.gateway; + network_options.ifname = get_single_udev_index(settings.filter.clone(), &udev_info.nics)?; + } + info!("Network interface used is '{}'", &network_options.ifname); + Ok(network_options) +} + +fn get_single_udev_index( + filter: BTreeMap, + udev_list: &BTreeMap>, +) -> Result { + if filter.is_empty() { + bail!("no filter defined"); + } + let mut dev_index: Option = 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, + udev_list: &BTreeMap>, + match_all: bool, +) -> Result> { + 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 { + answer::DiskSelection::Selection(disk_list) => { + let disk_name = disk_list[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"), + } + } + answer::DiskSelection::Filter(filter) => { + let disk_index = get_single_udev_index(filter.clone(), &udev_info.disks)?; + let disk = runtime_info + .disks + .iter() + .find(|item| item.index == disk_index); + config.target_hd = disk.cloned(); + } + } + info!("Selected disk: {}", config.target_hd.clone().unwrap().path); + Ok(()) +} + +fn set_selected_disks( + answer: &Answer, + udev_info: &UdevInfo, + runtime_info: &RuntimeInfo, + config: &mut InstallConfig, +) -> Result<()> { + match &answer.disks.disk_selection { + answer::DiskSelection::Selection(disk_list) => { + info!("Disk selection found"); + for disk_name in disk_list.clone() { + 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()); + } + } + } + answer::DiskSelection::Filter(filter) => { + info!("No disk list found, looking for disk filters"); + let filter_match = answer + .disks + .filter_match + .clone() + .unwrap_or(answer::FilterMatch::Any); + let disk_filters = filter.clone(); + let selected_disk_indexes = get_matched_udev_indexes( + disk_filters, + &udev_info.disks, + filter_match == answer::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."); + } + + let mut selected_disks: Vec = Vec::new(); + for i in config.disk_selection.keys() { + selected_disks.push( + runtime_info + .disks + .iter() + .find(|item| item.index.as_str() == i) + .unwrap() + .clone() + .path, + ); + } + info!( + "Selected disks: {}", + selected_disks + .iter() + .map(|x| x.to_string() + " ") + .collect::() + ); + + Ok(()) +} + +pub fn get_first_selected_disk(config: &InstallConfig) -> usize { + config + .disk_selection + .iter() + .next() + .expect("no disks found") + .0 + .parse::() + .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>) -> 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) -> 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 { + info!("Parsing answer file"); + info!("Setting File system"); + let filesystem = answer.disks.fs_type; + 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 &answer.disks.fs_options { + answer::FsOptions::LVM(lvm) => { + 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; + } + answer::FsOptions::ZFS(zfs) => { + 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: zfs.compress.unwrap_or(ZfsCompressOption::On), + checksum: zfs.checksum.unwrap_or(ZfsChecksumOption::On), + copies: zfs.copies.unwrap_or(1), + }); + } + answer::FsOptions::BRFS(btrfs) => { + 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