[pve-devel] [PATCH installer v6 11/36] auto-installer: add utils
Aaron Lauterer
a.lauterer at proxmox.com
Wed Apr 17 14:30:43 CEST 2024
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.
Tested-by: Christoph Heiss <c.heiss at proxmox.com>
Reviewed-by: Christoph Heiss <c.heiss at proxmox.com>
Signed-off-by: Aaron Lauterer <a.lauterer at proxmox.com>
---
proxmox-auto-installer/src/lib.rs | 1 +
proxmox-auto-installer/src/utils.rs | 424 ++++++++++++++++++++++++++++
2 files changed, 425 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..ae28b1e
--- /dev/null
+++ b/proxmox-auto-installer/src/utils.rs
@@ -0,0 +1,424 @@
+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<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 = 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<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 {
+ 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<String> = 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::<String>()
+ );
+
+ 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 child = match Command::new("/bin/bash")
+ .arg("-c")
+ .arg(cmd.clone())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .spawn()
+ {
+ Ok(child) => child,
+ Err(err) => bail!("error running command {cmd}: {err}"),
+ };
+ match child.wait_with_output() {
+ Ok(output) => {
+ if output.status.success() {
+ info!("{}", String::from_utf8(output.stdout).unwrap());
+ } else {
+ bail!("{}", String::from_utf8(output.stderr).unwrap());
+ }
+ },
+ Err(err) => 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 = 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::BTRFS(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
More information about the pve-devel
mailing list