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 7B986C278A for ; Tue, 23 Jan 2024 18:01:37 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 5171EAF50 for ; Tue, 23 Jan 2024 18:01:05 +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 ; Tue, 23 Jan 2024 18:00:59 +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 7F59849270 for ; Tue, 23 Jan 2024 18:00:59 +0100 (CET) From: Aaron Lauterer To: pve-devel@lists.proxmox.com Date: Tue, 23 Jan 2024 18:00:52 +0100 Message-Id: <20240123170053.490250-18-a.lauterer@proxmox.com> X-Mailer: git-send-email 2.39.2 In-Reply-To: <20240123170053.490250-1-a.lauterer@proxmox.com> References: <20240123170053.490250-1-a.lauterer@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.067 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 T_SCC_BODY_TEXT_LINE -0.01 - Subject: [pve-devel] [PATCH v1 installer 17/18] auto-installer: add proxmox-installer-filter helper tool 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: Tue, 23 Jan 2024 17:01:37 -0000 The proxmox-installer-filter tool is a helper utility to fetch UDEV properties quickly and show them to the user. Additionally it allows the user to test out filters for the auto installer answer file to see which devices would be selected. Since this tool should be able to run outside of the installer environment, it does not rely on the device information provided by the low-level installer. It instead fetches the list of disks and NICs by itself. The rules when a device is ignored, should match how the low-level installer handles it. Signed-off-by: Aaron Lauterer --- Makefile | 1 + proxmox-auto-installer/Cargo.toml | 2 + proxmox-auto-installer/src/answer.rs | 3 +- .../src/bin/proxmox-installer-filter.rs | 298 ++++++++++++++++++ 4 files changed, 303 insertions(+), 1 deletion(-) create mode 100644 proxmox-auto-installer/src/bin/proxmox-installer-filter.rs diff --git a/Makefile b/Makefile index b724789..2861f9e 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,7 @@ PREFIX = /usr BINDIR = $(PREFIX)/bin USR_BIN := \ proxmox-tui-installer\ + proxmox-installer-filter\ proxmox-fetch-answer\ proxmox-auto-installer diff --git a/proxmox-auto-installer/Cargo.toml b/proxmox-auto-installer/Cargo.toml index 158a0a8..570c34c 100644 --- a/proxmox-auto-installer/Cargo.toml +++ b/proxmox-auto-installer/Cargo.toml @@ -9,9 +9,11 @@ homepage = "https://www.proxmox.com" [dependencies] anyhow = "1.0" +clap = { version = "4.0", features = ["derive"] } glob = "0.3" proxmox-installer-common = { path = "../proxmox-installer-common" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" toml = "0.5.11" log = "0.4.20" +regex = "1.7" diff --git a/proxmox-auto-installer/src/answer.rs b/proxmox-auto-installer/src/answer.rs index 0f6c593..de88d7d 100644 --- a/proxmox-auto-installer/src/answer.rs +++ b/proxmox-auto-installer/src/answer.rs @@ -1,3 +1,4 @@ +use clap::ValueEnum; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; @@ -44,7 +45,7 @@ pub struct Disks { pub btrfs: Option, } -#[derive(Clone, Deserialize, Debug, PartialEq)] +#[derive(Clone, Deserialize, Debug, PartialEq, ValueEnum)] #[serde(rename_all = "lowercase")] pub enum FilterMatch { Any, diff --git a/proxmox-auto-installer/src/bin/proxmox-installer-filter.rs b/proxmox-auto-installer/src/bin/proxmox-installer-filter.rs new file mode 100644 index 0000000..315f414 --- /dev/null +++ b/proxmox-auto-installer/src/bin/proxmox-installer-filter.rs @@ -0,0 +1,298 @@ +use anyhow::{bail, Result}; +use clap::{Args, Parser, Subcommand, ValueEnum}; +use glob::Pattern; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use std::{collections::BTreeMap, fs, path::PathBuf, process::Command}; + +use proxmox_auto_installer::{ + answer::FilterMatch, + utils::{get_matched_udev_indexes, get_single_udev_index}, +}; + +/// Tool to get info for devices and test filter matches +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +enum Commands { + Info(CommandInfo), + Match(CommandMatch), +} + +/// Helper utility to show device information and test filters for the Proxmox auto installer +#[derive(Args, Debug)] +struct CommandInfo { + /// For which device type information should be shown + #[arg(name="type", short, long, value_enum, default_value_t=AllDeviceTypes::All)] + device: AllDeviceTypes, +} + +/// Test which devices the given filter match against +/// +/// Filters support the following syntax: +/// ? Match a single character +/// * Match any number of characters +/// [a], [0-9] Specifc character or range of characters +/// [!a] Negate a specific character of range +/// +/// To avoid globbing characters being interpreted by the shell, use single quotes. +/// Multiple filters can be defined. +/// +/// Examples: +/// Match disks against the serial number and device name, both must match: +/// +/// proxmox-installer-filter match --filter-match all disk 'ID_SERIAL_SHORT=*2222*' 'DEVNAME=*nvme*' +#[derive(Args, Debug)] +#[command(verbatim_doc_comment)] +struct CommandMatch { + /// Device type to match the filter against + //#[arg(name = "type", long, short, value_enum, required = true)] + r#type: Devicetype, + + /// Filter in the format KEY=VALUE where the key is the UDEV key and VALUE the filter string. + //#[arg(long, num_args = 1.., verbatim_doc_comment, required = true)] + filter: Vec, + + /// Defines if any filter or all filters must match. + #[arg(long, value_enum, default_value_t=FilterMatch::Any)] + filter_match: FilterMatch, +} + +#[derive(Args, Debug)] +struct GlobalOpts { + /// Output format + #[arg(long, short, value_enum)] + format: OutputFormat, +} + +#[derive(Clone, Debug, ValueEnum, PartialEq)] +enum AllDeviceTypes { + All, + Network, + Disk, +} + +#[derive(Clone, Debug, ValueEnum)] +enum Devicetype { + Network, + Disk, +} + +#[derive(Clone, Debug, ValueEnum)] +enum OutputFormat { + Pretty, + Json, +} + +#[derive(Serialize)] +struct Devs { + disks: Option>>, + nics: Option>>, +} + +fn main() { + let args = Cli::parse(); + match &args.command { + Commands::Info(args) => info(args), + Commands::Match(args) => match_filter(args), + } +} + +fn info(args: &CommandInfo) { + let mut devs = Devs { + disks: None, + nics: None, + }; + + if args.device == AllDeviceTypes::Network || args.device == AllDeviceTypes::All { + match get_nics() { + Ok(res) => devs.nics = Some(res), + Err(err) => panic!("Error getting NIC data: {err}"), + } + } + if args.device == AllDeviceTypes::Disk || args.device == AllDeviceTypes::All { + match get_disks() { + Ok(res) => devs.disks = Some(res), + Err(err) => panic!("Error getting disk data: {err}"), + } + } + println!("{}", serde_json::to_string_pretty(&devs).unwrap()); +} + +fn match_filter(args: &CommandMatch) { + let devs: BTreeMap> = match args.r#type { + Devicetype::Disk => get_disks().unwrap(), + Devicetype::Network => get_nics().unwrap(), + }; + // parse filters + + let mut filters: BTreeMap = BTreeMap::new(); + + for f in &args.filter { + match f.split_once('=') { + Some((key, value)) => { + if key.is_empty() || value.is_empty() { + eprintln!("Filter key or value is empty in filter: '{f}'"); + std::process::exit(1); + } + filters.insert(String::from(key), String::from(value)); + } + None => { + eprintln!("Could not find separator '=' in filter: '{f}'"); + std::process::exit(1); + } + } + } + + // align return values + let result = match args.r#type { + Devicetype::Disk => { + get_matched_udev_indexes(filters, &devs, args.filter_match == FilterMatch::All) + } + Devicetype::Network => get_single_udev_index(filters, &devs).map(|r| vec![r]), + }; + + match result { + Ok(result) => println!("{}", serde_json::to_string_pretty(&result).unwrap()), + Err(err) => eprintln!("Error matching filters: {err}"), + } +} + +fn get_disks() -> Result>> { + let unwantend_block_devs = vec![ + "ram[0-9]*", + "loop[0-9]*", + "md[0-9]*", + "dm-*", + "fd[0-9]*", + "sr[0-9]*", + ]; + + // compile Regex here once and not inside the loop + let re_disk = Regex::new(r"(?m)^E: DEVTYPE=disk")?; + let re_cdrom = Regex::new(r"(?m)^E: ID_CDROM")?; + let re_iso9660 = Regex::new(r"(?m)^E: ID_FS_TYPE=iso9660")?; + + let re_name = Regex::new(r"(?m)^N: (.*)$")?; + let re_props = Regex::new(r"(?m)^E: (.*)=(.*)$")?; + + let mut disks: BTreeMap> = BTreeMap::new(); + + 'outer: for entry in fs::read_dir("/sys/block")? { + let entry = entry.unwrap(); + let filename = entry.file_name().into_string().unwrap(); + + for p in &unwantend_block_devs { + if Pattern::new(p)?.matches(&filename) { + continue 'outer; + } + } + + let output = match get_udev_properties(&entry.path()) { + Ok(output) => output, + Err(err) => { + eprint!("{err}"); + continue 'outer; + } + }; + + if !re_disk.is_match(&output) { + continue 'outer; + }; + if re_cdrom.is_match(&output) { + continue 'outer; + }; + if re_iso9660.is_match(&output) { + continue 'outer; + }; + + let mut name = filename; + if let Some(cap) = re_name.captures(&output) { + if let Some(res) = cap.get(1) { + name = String::from(res.as_str()); + } + } + + let mut udev_props: BTreeMap = BTreeMap::new(); + + for line in output.lines() { + if let Some(caps) = re_props.captures(line) { + let key = String::from(caps.get(1).unwrap().as_str()); + let value = String::from(caps.get(2).unwrap().as_str()); + udev_props.insert(key, value); + } + } + + disks.insert(name, udev_props); + } + Ok(disks) +} + +#[derive(Deserialize, Debug)] +struct IpLinksInfo { + ifname: String, +} + +fn get_nics() -> Result>> { + let re_props = Regex::new(r"(?m)^E: (.*)=(.*)$")?; + let mut nics: BTreeMap> = BTreeMap::new(); + + let ip_output = Command::new("/usr/sbin/ip") + .arg("-j") + .arg("link") + .output()?; + let parsed_links: Vec = + serde_json::from_str(String::from_utf8(ip_output.stdout)?.as_str())?; + let mut links: Vec = Vec::new(); + + for link in parsed_links { + if link.ifname == *"lo" { + continue; + } + links.push(link.ifname); + } + + for link in links { + let path = format!("/sys/class/net/{link}"); + + let output = match get_udev_properties(&PathBuf::from(path)) { + Ok(output) => output, + Err(err) => { + eprint!("{err}"); + continue; + } + }; + + let mut udev_props: BTreeMap = BTreeMap::new(); + + for line in output.lines() { + if let Some(caps) = re_props.captures(line) { + let key = String::from(caps.get(1).unwrap().as_str()); + let value = String::from(caps.get(2).unwrap().as_str()); + udev_props.insert(key, value); + } + } + + nics.insert(link, udev_props); + } + Ok(nics) +} + +fn get_udev_properties(path: &PathBuf) -> Result { + let udev_output = Command::new("udevadm") + .arg("info") + .arg("--path") + .arg(path) + .arg("--query") + .arg("all") + .output()?; + if !udev_output.status.success() { + bail!("could not run udevadm successfully for {}", path.display()); + } + Ok(String::from_utf8(udev_output.stdout)?) +} -- 2.39.2