]> git.proxmox.com Git - pve-installer.git/commitdiff
rename proxmox-autoinst-helper to proxmox-auto-install-assistant
authorThomas Lamprecht <t.lamprecht@proxmox.com>
Thu, 18 Apr 2024 18:10:37 +0000 (20:10 +0200)
committerThomas Lamprecht <t.lamprecht@proxmox.com>
Mon, 22 Apr 2024 12:31:37 +0000 (14:31 +0200)
stay on the verbose side

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
Cargo.toml
Makefile
proxmox-auto-install-assistant/Cargo.toml [new file with mode: 0644]
proxmox-auto-install-assistant/src/main.rs [new file with mode: 0644]
proxmox-autoinst-helper/Cargo.toml [deleted file]
proxmox-autoinst-helper/src/main.rs [deleted file]

index b3afc7c5618ace1bb5a1fb4fd73b914f460faeac..09fdd84657e697b69868d0fae19c78085b62dab6 100644 (file)
@@ -1,7 +1,7 @@
 [workspace]
 members = [
     "proxmox-auto-installer",
-    "proxmox-autoinst-helper",
+    "proxmox-auto-install-assistant",
     "proxmox-chroot",
     "proxmox-fetch-answer",
     "proxmox-installer-common",
index d69dc6fff8a08370e724acce0e18104880d0eb83..d07e166b62791259243e2261a1ff7e2d7ccd16e9 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -21,8 +21,8 @@ BINDIR = $(PREFIX)/bin
 USR_BIN := \
           proxmox-chroot\
           proxmox-tui-installer\
-          proxmox-autoinst-helper\
           proxmox-fetch-answer\
+          proxmox-auto-install-assistant \
           proxmox-auto-installer
 
 COMPILED_BINS := \
@@ -53,7 +53,7 @@ $(BUILDDIR):
          proxinstall \
          proxmox-low-level-installer \
          proxmox-auto-installer/ \
-         proxmox-autoinst-helper/ \
+         proxmox-auto-install-assistant/ \
          proxmox-fetch-answer/ \
          proxmox-chroot \
          proxmox-tui-installer/ \
@@ -129,7 +129,7 @@ cargo-build:
        $(CARGO) build --package proxmox-tui-installer --bin proxmox-tui-installer \
                --package proxmox-auto-installer --bin proxmox-auto-installer \
                --package proxmox-fetch-answer --bin proxmox-fetch-answer \
-               --package proxmox-autoinst-helper --bin proxmox-autoinst-helper \
+               --package proxmox-auto-install-assistant --bin proxmox-auto-install-assistant \
                --package proxmox-chroot --bin proxmox-chroot $(CARGO_BUILD_ARGS)
 
 %-banner.png: %-banner.svg
diff --git a/proxmox-auto-install-assistant/Cargo.toml b/proxmox-auto-install-assistant/Cargo.toml
new file mode 100644 (file)
index 0000000..eaca7f8
--- /dev/null
@@ -0,0 +1,22 @@
+[package]
+name = "proxmox-auto-install-assistant"
+version = "0.1.0"
+edition = "2021"
+authors = [
+    "Aaron Lauterer <a.lauterer@proxmox.com>",
+    "Proxmox Support Team <support@proxmox.com>",
+]
+license = "AGPL-3"
+exclude = [ "build", "debian" ]
+homepage = "https://www.proxmox.com"
+
+[dependencies]
+anyhow = "1.0"
+clap = { version = "4.0", features = ["derive"] }
+glob = "0.3"
+log = "0.4.20"
+proxmox-auto-installer = { path = "../proxmox-auto-installer" }
+regex = "1.7"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+toml = "0.7"
diff --git a/proxmox-auto-install-assistant/src/main.rs b/proxmox-auto-install-assistant/src/main.rs
new file mode 100644 (file)
index 0000000..9b04a24
--- /dev/null
@@ -0,0 +1,561 @@
+use anyhow::{bail, Result};
+use clap::{Args, Parser, Subcommand, ValueEnum};
+use glob::Pattern;
+use regex::Regex;
+use serde::Serialize;
+use std::{
+    collections::BTreeMap,
+    fs,
+    io::Read,
+    path::{Path, PathBuf},
+    process::{Command, Stdio},
+};
+
+use proxmox_auto_installer::{
+    answer::Answer,
+    answer::FilterMatch,
+    sysinfo,
+    utils::{
+        get_matched_udev_indexes, get_nic_list, get_single_udev_index, AutoInstModes,
+        AutoInstSettings,
+    },
+};
+
+static PROXMOX_ISO_FLAG: &str = "/autoinst-capable";
+
+/// This tool can be used to prepare a Proxmox installation ISO for automated installations.
+/// Additional uses are to validate the format of an answer file or to test match filters and
+/// print information on the properties to match against for the current hardware.
+#[derive(Parser, Debug)]
+#[command(author, version, about, long_about = None)]
+struct Cli {
+    #[command(subcommand)]
+    command: Commands,
+}
+
+#[derive(Subcommand, Debug)]
+enum Commands {
+    PrepareIso(CommandPrepareISO),
+    ValidateAnswer(CommandValidateAnswer),
+    DeviceMatch(CommandDeviceMatch),
+    DeviceInfo(CommandDeviceInfo),
+    Identifiers(CommandIdentifiers),
+}
+
+/// Show device information that can be used for filters
+#[derive(Args, Debug)]
+struct CommandDeviceInfo {
+    /// 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 matches 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-autoinst-helper match --filter-match all disk 'ID_SERIAL_SHORT=*2222*' 'DEVNAME=*nvme*'
+#[derive(Args, Debug)]
+#[command(verbatim_doc_comment)]
+struct CommandDeviceMatch {
+    /// Device type to match the filter against
+    r#type: Devicetype,
+
+    /// Filter in the format KEY=VALUE where the key is the UDEV key and VALUE the filter string.
+    /// Multiple filters are possible, separated by a space.
+    filter: Vec<String>,
+
+    /// Defines if any filter or all filters must match.
+    #[arg(long, value_enum, default_value_t=FilterMatch::Any)]
+    filter_match: FilterMatch,
+}
+
+/// Validate if an answer file is formatted correctly.
+#[derive(Args, Debug)]
+struct CommandValidateAnswer {
+    /// Path to the answer file
+    path: PathBuf,
+    #[arg(short, long, default_value_t = false)]
+    debug: bool,
+}
+
+/// Prepare an ISO for automated installation.
+///
+/// The final ISO will try to fetch an answer file automatically. It will first search for a
+/// partition / file-system called "PROXMOXINST" (or lowercase) and a file in the root named
+/// "answer.toml".
+///
+/// If that is not found, it will try to fetch an answer file via an HTTP Post request. The URL for
+/// it can be defined for the ISO with the '--url', '-u' argument. If not present, it will try to
+/// get a URL from a DHCP option (250, TXT) or as a DNS TXT record at 'proxmoxinst.{search
+/// domain}'.
+///
+/// The TLS certificate fingerprint can either be defined via the '--cert-fingerprint', '-c'
+/// argument or alternatively via the custom DHCP option (251, TXT) or in a DNS TXT record located
+/// at 'proxmoxinst-fp.{search domain}'.
+///
+/// The latter options to provide the TLS fingerprint will only be used if the same method was used
+/// to retrieve the URL. For example, the DNS TXT record for the fingerprint will only be used, if
+/// no one was configured with the '--cert-fingerprint' parameter and if the URL was retrieved via
+/// the DNS TXT record.
+///
+/// The behavior of how to fetch an answer file can be overridden with the '--install-mode', '-i'
+/// parameter. The answer file can be{n}
+/// * integrated into the ISO itself ('included'){n}
+/// * needs to be present in a partition / file-system called 'PROXMOXINST' ('partition'){n}
+/// * only be requested via an HTTP Post request ('http').
+#[derive(Args, Debug)]
+struct CommandPrepareISO {
+    /// Path to the source ISO
+    source: PathBuf,
+
+    /// Path to store the final ISO to.
+    #[arg(short, long)]
+    target: Option<PathBuf>,
+
+    /// Where to fetch the answer file from.
+    #[arg(short, long, value_enum, default_value_t=AutoInstModes::Auto)]
+    install_mode: AutoInstModes,
+
+    /// Include the specified answer file in the ISO. Requires the '--install-mode', '-i' parameter
+    /// to be set to 'included'.
+    #[arg(short, long)]
+    answer_file: Option<PathBuf>,
+
+    /// Specify URL for fetching the answer file via HTTP
+    #[arg(short, long)]
+    url: Option<String>,
+
+    /// Pin the ISO to the specified SHA256 TLS certificate fingerprint.
+    #[arg(short, long)]
+    cert_fingerprint: Option<String>,
+
+    /// Tmp directory to use.
+    #[arg(long)]
+    tmp: Option<String>,
+}
+
+/// Show identifiers for the current machine. This information is part of the POST request to fetch
+/// an answer file.
+#[derive(Args, Debug)]
+struct CommandIdentifiers {}
+
+#[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<BTreeMap<String, BTreeMap<String, String>>>,
+    nics: Option<BTreeMap<String, BTreeMap<String, String>>>,
+}
+
+fn main() {
+    let args = Cli::parse();
+    let res = match &args.command {
+        Commands::PrepareIso(args) => prepare_iso(args),
+        Commands::ValidateAnswer(args) => validate_answer(args),
+        Commands::DeviceInfo(args) => info(args),
+        Commands::DeviceMatch(args) => match_filter(args),
+        Commands::Identifiers(args) => show_identifiers(args),
+    };
+    if let Err(err) = res {
+        eprintln!("{err}");
+        std::process::exit(1);
+    }
+}
+
+fn info(args: &CommandDeviceInfo) -> Result<()> {
+    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) => bail!("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) => bail!("Error getting disk data: {err}"),
+        }
+    }
+    println!("{}", serde_json::to_string_pretty(&devs).unwrap());
+    Ok(())
+}
+
+fn match_filter(args: &CommandDeviceMatch) -> Result<()> {
+    let devs: BTreeMap<String, BTreeMap<String, String>> = match args.r#type {
+        Devicetype::Disk => get_disks().unwrap(),
+        Devicetype::Network => get_nics().unwrap(),
+    };
+    // parse filters
+
+    let mut filters: BTreeMap<String, String> = BTreeMap::new();
+
+    for f in &args.filter {
+        match f.split_once('=') {
+            Some((key, value)) => {
+                if key.is_empty() || value.is_empty() {
+                    bail!("Filter key or value is empty in filter: '{f}'");
+                }
+                filters.insert(String::from(key), String::from(value));
+            }
+            None => {
+                bail!("Could not find separator '=' in filter: '{f}'");
+            }
+        }
+    }
+
+    // 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) => bail!("Error matching filters: {err}"),
+    }
+    Ok(())
+}
+
+fn validate_answer(args: &CommandValidateAnswer) -> Result<()> {
+    let answer = parse_answer(&args.path)?;
+    if args.debug {
+        println!("Parsed data from answer file:\n{:#?}", answer);
+    }
+    Ok(())
+}
+
+fn show_identifiers(_args: &CommandIdentifiers) -> Result<()> {
+    match sysinfo::get_sysinfo(true) {
+        Ok(res) => println!("{res}"),
+        Err(err) => eprintln!("Error fetching system identifiers: {err}"),
+    }
+    Ok(())
+}
+
+fn prepare_iso(args: &CommandPrepareISO) -> Result<()> {
+    check_prepare_requirements(args)?;
+
+    if args.install_mode == AutoInstModes::Included {
+        if args.answer_file.is_none() {
+            bail!("Missing path to answer file needed for 'direct' install mode.");
+        }
+        if args.cert_fingerprint.is_some() {
+            bail!("No certificate fingerprint needed for direct install mode. Drop the parameter!");
+        }
+        if args.url.is_some() {
+            bail!("No URL needed for direct install mode. Drop the parameter!");
+        }
+    } else if args.install_mode == AutoInstModes::Partition {
+        if args.cert_fingerprint.is_some() {
+            bail!(
+                "No certificate fingerprint needed for partition install mode. Drop the parameter!"
+            );
+        }
+        if args.url.is_some() {
+            bail!("No URL needed for partition install mode. Drop the parameter!");
+        }
+    }
+    if args.answer_file.is_some() && args.install_mode != AutoInstModes::Included {
+        bail!("Set '-i', '--install-mode' to 'included' to place the answer file directly in the ISO.");
+    }
+
+    if let Some(file) = &args.answer_file {
+        println!("Checking provided answer file...");
+        parse_answer(file)?;
+    }
+
+    let mut tmp_base = PathBuf::new();
+    if args.tmp.is_some() {
+        tmp_base.push(args.tmp.as_ref().unwrap());
+    } else {
+        tmp_base.push(args.source.parent().unwrap());
+        tmp_base.push(".proxmox-iso-prepare");
+    }
+    fs::create_dir_all(&tmp_base)?;
+
+    let mut tmp_iso = tmp_base.clone();
+    tmp_iso.push("proxmox.iso");
+    let mut tmp_answer = tmp_base.clone();
+    tmp_answer.push("answer.toml");
+
+    println!("Copying source ISO to temporary location...");
+    fs::copy(&args.source, &tmp_iso)?;
+    println!("Done copying source ISO");
+
+    println!("Preparing ISO...");
+    let install_mode = AutoInstSettings {
+        mode: args.install_mode.clone(),
+        http_url: args.url.clone(),
+        cert_fingerprint: args.cert_fingerprint.clone(),
+    };
+    let mut instmode_file_tmp = tmp_base.clone();
+    instmode_file_tmp.push("autoinst-mode.toml");
+    fs::write(&instmode_file_tmp, toml::to_string_pretty(&install_mode)?)?;
+
+    inject_file_to_iso(&tmp_iso, &instmode_file_tmp, "/autoinst-mode.toml")?;
+
+    if let Some(answer) = &args.answer_file {
+        fs::copy(answer, &tmp_answer)?;
+        inject_file_to_iso(&tmp_iso, &tmp_answer, "/answer.toml")?;
+    }
+
+    println!("Done preparing iso.");
+    println!("Move ISO to target location...");
+    let iso_target = final_iso_location(args);
+    fs::rename(&tmp_iso, &iso_target)?;
+    println!("Cleaning up...");
+    fs::remove_dir_all(&tmp_base)?;
+    println!("Final ISO is available at {}.", &iso_target.display());
+
+    Ok(())
+}
+
+fn final_iso_location(args: &CommandPrepareISO) -> PathBuf {
+    if let Some(specified) = args.target.clone() {
+        return specified;
+    }
+    let mut suffix: String = match args.install_mode {
+        AutoInstModes::Auto => "auto".into(),
+        AutoInstModes::Http => "auto-http".into(),
+        AutoInstModes::Included => "auto-answer-included".into(),
+        AutoInstModes::Partition => "auto-part".into(),
+    };
+
+    if args.url.is_some() {
+        suffix.push_str("-url");
+    }
+    if args.cert_fingerprint.is_some() {
+        suffix.push_str("-fp");
+    }
+
+    let base = args.source.parent().unwrap();
+    let iso = args.source.file_stem().unwrap();
+
+    let mut target = base.to_path_buf();
+    target.push(format!("{}-{}.iso", iso.to_str().unwrap(), suffix));
+
+    target.to_path_buf()
+}
+
+fn inject_file_to_iso(iso: &PathBuf, file: &PathBuf, location: &str) -> Result<()> {
+    let result = Command::new("xorriso")
+        .arg("--boot_image")
+        .arg("any")
+        .arg("keep")
+        .arg("-dev")
+        .arg(iso)
+        .arg("-map")
+        .arg(file)
+        .arg(location)
+        .output()?;
+    if !result.status.success() {
+        bail!(
+            "Error injecting {} into {}: {}",
+            file.display(),
+            iso.display(),
+            String::from_utf8(result.stderr)?
+        );
+    }
+    Ok(())
+}
+
+fn get_disks() -> Result<BTreeMap<String, BTreeMap<String, String>>> {
+    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<String, BTreeMap<String, String>> = 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<String, String> = 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)
+}
+
+fn get_nics() -> Result<BTreeMap<String, BTreeMap<String, String>>> {
+    let re_props = Regex::new(r"(?m)^E: (.*)=(.*)$")?;
+    let mut nics: BTreeMap<String, BTreeMap<String, String>> = BTreeMap::new();
+
+    let links = get_nic_list()?;
+    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<String, String> = 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<String> {
+    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)?)
+}
+
+fn parse_answer(path: &PathBuf) -> Result<Answer> {
+    let mut file = match fs::File::open(path) {
+        Ok(file) => file,
+        Err(err) => bail!("Opening answer file '{}' failed: {err}", path.display()),
+    };
+    let mut contents = String::new();
+    if let Err(err) = file.read_to_string(&mut contents) {
+        bail!("Reading from file '{}' failed: {err}", path.display());
+    }
+    match toml::from_str(&contents) {
+        Ok(answer) => {
+            println!("The file was parsed successfully, no syntax errors found!");
+            Ok(answer)
+        }
+        Err(err) => bail!("Error parsing answer file: {err}"),
+    }
+}
+
+fn check_prepare_requirements(args: &CommandPrepareISO) -> Result<()> {
+    match Path::try_exists(&args.source) {
+        Ok(true) => (),
+        Ok(false) => bail!("Source file does not exist."),
+        Err(_) => bail!("Source file does not exist."),
+    }
+
+    match Command::new("xorriso")
+        .arg("-dev")
+        .arg(&args.source)
+        .arg("-find")
+        .arg(PROXMOX_ISO_FLAG)
+        .stderr(Stdio::null())
+        .stdout(Stdio::null())
+        .status()
+    {
+        Ok(v) => {
+            if !v.success() {
+                bail!("The source ISO file is not able to be installed automatically. Please try a more current one.");
+            }
+        }
+        Err(_) => bail!("Could not run 'xorriso'. Please install it."),
+    };
+
+    Ok(())
+}
diff --git a/proxmox-autoinst-helper/Cargo.toml b/proxmox-autoinst-helper/Cargo.toml
deleted file mode 100644 (file)
index a32a634..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-[package]
-name = "proxmox-autoinst-helper"
-version = "0.1.0"
-edition = "2021"
-authors = [
-    "Aaron Lauterer <a.lauterer@proxmox.com>",
-    "Proxmox Support Team <support@proxmox.com>",
-]
-license = "AGPL-3"
-exclude = [ "build", "debian" ]
-homepage = "https://www.proxmox.com"
-
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
-[dependencies]
-anyhow = "1.0"
-clap = { version = "4.0", features = ["derive"] }
-glob = "0.3"
-log = "0.4.20"
-proxmox-auto-installer = { path = "../proxmox-auto-installer" }
-regex = "1.7"
-serde = { version = "1.0", features = ["derive"] }
-serde_json = "1.0"
-toml = "0.7"
diff --git a/proxmox-autoinst-helper/src/main.rs b/proxmox-autoinst-helper/src/main.rs
deleted file mode 100644 (file)
index 9b04a24..0000000
+++ /dev/null
@@ -1,561 +0,0 @@
-use anyhow::{bail, Result};
-use clap::{Args, Parser, Subcommand, ValueEnum};
-use glob::Pattern;
-use regex::Regex;
-use serde::Serialize;
-use std::{
-    collections::BTreeMap,
-    fs,
-    io::Read,
-    path::{Path, PathBuf},
-    process::{Command, Stdio},
-};
-
-use proxmox_auto_installer::{
-    answer::Answer,
-    answer::FilterMatch,
-    sysinfo,
-    utils::{
-        get_matched_udev_indexes, get_nic_list, get_single_udev_index, AutoInstModes,
-        AutoInstSettings,
-    },
-};
-
-static PROXMOX_ISO_FLAG: &str = "/autoinst-capable";
-
-/// This tool can be used to prepare a Proxmox installation ISO for automated installations.
-/// Additional uses are to validate the format of an answer file or to test match filters and
-/// print information on the properties to match against for the current hardware.
-#[derive(Parser, Debug)]
-#[command(author, version, about, long_about = None)]
-struct Cli {
-    #[command(subcommand)]
-    command: Commands,
-}
-
-#[derive(Subcommand, Debug)]
-enum Commands {
-    PrepareIso(CommandPrepareISO),
-    ValidateAnswer(CommandValidateAnswer),
-    DeviceMatch(CommandDeviceMatch),
-    DeviceInfo(CommandDeviceInfo),
-    Identifiers(CommandIdentifiers),
-}
-
-/// Show device information that can be used for filters
-#[derive(Args, Debug)]
-struct CommandDeviceInfo {
-    /// 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 matches 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-autoinst-helper match --filter-match all disk 'ID_SERIAL_SHORT=*2222*' 'DEVNAME=*nvme*'
-#[derive(Args, Debug)]
-#[command(verbatim_doc_comment)]
-struct CommandDeviceMatch {
-    /// Device type to match the filter against
-    r#type: Devicetype,
-
-    /// Filter in the format KEY=VALUE where the key is the UDEV key and VALUE the filter string.
-    /// Multiple filters are possible, separated by a space.
-    filter: Vec<String>,
-
-    /// Defines if any filter or all filters must match.
-    #[arg(long, value_enum, default_value_t=FilterMatch::Any)]
-    filter_match: FilterMatch,
-}
-
-/// Validate if an answer file is formatted correctly.
-#[derive(Args, Debug)]
-struct CommandValidateAnswer {
-    /// Path to the answer file
-    path: PathBuf,
-    #[arg(short, long, default_value_t = false)]
-    debug: bool,
-}
-
-/// Prepare an ISO for automated installation.
-///
-/// The final ISO will try to fetch an answer file automatically. It will first search for a
-/// partition / file-system called "PROXMOXINST" (or lowercase) and a file in the root named
-/// "answer.toml".
-///
-/// If that is not found, it will try to fetch an answer file via an HTTP Post request. The URL for
-/// it can be defined for the ISO with the '--url', '-u' argument. If not present, it will try to
-/// get a URL from a DHCP option (250, TXT) or as a DNS TXT record at 'proxmoxinst.{search
-/// domain}'.
-///
-/// The TLS certificate fingerprint can either be defined via the '--cert-fingerprint', '-c'
-/// argument or alternatively via the custom DHCP option (251, TXT) or in a DNS TXT record located
-/// at 'proxmoxinst-fp.{search domain}'.
-///
-/// The latter options to provide the TLS fingerprint will only be used if the same method was used
-/// to retrieve the URL. For example, the DNS TXT record for the fingerprint will only be used, if
-/// no one was configured with the '--cert-fingerprint' parameter and if the URL was retrieved via
-/// the DNS TXT record.
-///
-/// The behavior of how to fetch an answer file can be overridden with the '--install-mode', '-i'
-/// parameter. The answer file can be{n}
-/// * integrated into the ISO itself ('included'){n}
-/// * needs to be present in a partition / file-system called 'PROXMOXINST' ('partition'){n}
-/// * only be requested via an HTTP Post request ('http').
-#[derive(Args, Debug)]
-struct CommandPrepareISO {
-    /// Path to the source ISO
-    source: PathBuf,
-
-    /// Path to store the final ISO to.
-    #[arg(short, long)]
-    target: Option<PathBuf>,
-
-    /// Where to fetch the answer file from.
-    #[arg(short, long, value_enum, default_value_t=AutoInstModes::Auto)]
-    install_mode: AutoInstModes,
-
-    /// Include the specified answer file in the ISO. Requires the '--install-mode', '-i' parameter
-    /// to be set to 'included'.
-    #[arg(short, long)]
-    answer_file: Option<PathBuf>,
-
-    /// Specify URL for fetching the answer file via HTTP
-    #[arg(short, long)]
-    url: Option<String>,
-
-    /// Pin the ISO to the specified SHA256 TLS certificate fingerprint.
-    #[arg(short, long)]
-    cert_fingerprint: Option<String>,
-
-    /// Tmp directory to use.
-    #[arg(long)]
-    tmp: Option<String>,
-}
-
-/// Show identifiers for the current machine. This information is part of the POST request to fetch
-/// an answer file.
-#[derive(Args, Debug)]
-struct CommandIdentifiers {}
-
-#[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<BTreeMap<String, BTreeMap<String, String>>>,
-    nics: Option<BTreeMap<String, BTreeMap<String, String>>>,
-}
-
-fn main() {
-    let args = Cli::parse();
-    let res = match &args.command {
-        Commands::PrepareIso(args) => prepare_iso(args),
-        Commands::ValidateAnswer(args) => validate_answer(args),
-        Commands::DeviceInfo(args) => info(args),
-        Commands::DeviceMatch(args) => match_filter(args),
-        Commands::Identifiers(args) => show_identifiers(args),
-    };
-    if let Err(err) = res {
-        eprintln!("{err}");
-        std::process::exit(1);
-    }
-}
-
-fn info(args: &CommandDeviceInfo) -> Result<()> {
-    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) => bail!("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) => bail!("Error getting disk data: {err}"),
-        }
-    }
-    println!("{}", serde_json::to_string_pretty(&devs).unwrap());
-    Ok(())
-}
-
-fn match_filter(args: &CommandDeviceMatch) -> Result<()> {
-    let devs: BTreeMap<String, BTreeMap<String, String>> = match args.r#type {
-        Devicetype::Disk => get_disks().unwrap(),
-        Devicetype::Network => get_nics().unwrap(),
-    };
-    // parse filters
-
-    let mut filters: BTreeMap<String, String> = BTreeMap::new();
-
-    for f in &args.filter {
-        match f.split_once('=') {
-            Some((key, value)) => {
-                if key.is_empty() || value.is_empty() {
-                    bail!("Filter key or value is empty in filter: '{f}'");
-                }
-                filters.insert(String::from(key), String::from(value));
-            }
-            None => {
-                bail!("Could not find separator '=' in filter: '{f}'");
-            }
-        }
-    }
-
-    // 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) => bail!("Error matching filters: {err}"),
-    }
-    Ok(())
-}
-
-fn validate_answer(args: &CommandValidateAnswer) -> Result<()> {
-    let answer = parse_answer(&args.path)?;
-    if args.debug {
-        println!("Parsed data from answer file:\n{:#?}", answer);
-    }
-    Ok(())
-}
-
-fn show_identifiers(_args: &CommandIdentifiers) -> Result<()> {
-    match sysinfo::get_sysinfo(true) {
-        Ok(res) => println!("{res}"),
-        Err(err) => eprintln!("Error fetching system identifiers: {err}"),
-    }
-    Ok(())
-}
-
-fn prepare_iso(args: &CommandPrepareISO) -> Result<()> {
-    check_prepare_requirements(args)?;
-
-    if args.install_mode == AutoInstModes::Included {
-        if args.answer_file.is_none() {
-            bail!("Missing path to answer file needed for 'direct' install mode.");
-        }
-        if args.cert_fingerprint.is_some() {
-            bail!("No certificate fingerprint needed for direct install mode. Drop the parameter!");
-        }
-        if args.url.is_some() {
-            bail!("No URL needed for direct install mode. Drop the parameter!");
-        }
-    } else if args.install_mode == AutoInstModes::Partition {
-        if args.cert_fingerprint.is_some() {
-            bail!(
-                "No certificate fingerprint needed for partition install mode. Drop the parameter!"
-            );
-        }
-        if args.url.is_some() {
-            bail!("No URL needed for partition install mode. Drop the parameter!");
-        }
-    }
-    if args.answer_file.is_some() && args.install_mode != AutoInstModes::Included {
-        bail!("Set '-i', '--install-mode' to 'included' to place the answer file directly in the ISO.");
-    }
-
-    if let Some(file) = &args.answer_file {
-        println!("Checking provided answer file...");
-        parse_answer(file)?;
-    }
-
-    let mut tmp_base = PathBuf::new();
-    if args.tmp.is_some() {
-        tmp_base.push(args.tmp.as_ref().unwrap());
-    } else {
-        tmp_base.push(args.source.parent().unwrap());
-        tmp_base.push(".proxmox-iso-prepare");
-    }
-    fs::create_dir_all(&tmp_base)?;
-
-    let mut tmp_iso = tmp_base.clone();
-    tmp_iso.push("proxmox.iso");
-    let mut tmp_answer = tmp_base.clone();
-    tmp_answer.push("answer.toml");
-
-    println!("Copying source ISO to temporary location...");
-    fs::copy(&args.source, &tmp_iso)?;
-    println!("Done copying source ISO");
-
-    println!("Preparing ISO...");
-    let install_mode = AutoInstSettings {
-        mode: args.install_mode.clone(),
-        http_url: args.url.clone(),
-        cert_fingerprint: args.cert_fingerprint.clone(),
-    };
-    let mut instmode_file_tmp = tmp_base.clone();
-    instmode_file_tmp.push("autoinst-mode.toml");
-    fs::write(&instmode_file_tmp, toml::to_string_pretty(&install_mode)?)?;
-
-    inject_file_to_iso(&tmp_iso, &instmode_file_tmp, "/autoinst-mode.toml")?;
-
-    if let Some(answer) = &args.answer_file {
-        fs::copy(answer, &tmp_answer)?;
-        inject_file_to_iso(&tmp_iso, &tmp_answer, "/answer.toml")?;
-    }
-
-    println!("Done preparing iso.");
-    println!("Move ISO to target location...");
-    let iso_target = final_iso_location(args);
-    fs::rename(&tmp_iso, &iso_target)?;
-    println!("Cleaning up...");
-    fs::remove_dir_all(&tmp_base)?;
-    println!("Final ISO is available at {}.", &iso_target.display());
-
-    Ok(())
-}
-
-fn final_iso_location(args: &CommandPrepareISO) -> PathBuf {
-    if let Some(specified) = args.target.clone() {
-        return specified;
-    }
-    let mut suffix: String = match args.install_mode {
-        AutoInstModes::Auto => "auto".into(),
-        AutoInstModes::Http => "auto-http".into(),
-        AutoInstModes::Included => "auto-answer-included".into(),
-        AutoInstModes::Partition => "auto-part".into(),
-    };
-
-    if args.url.is_some() {
-        suffix.push_str("-url");
-    }
-    if args.cert_fingerprint.is_some() {
-        suffix.push_str("-fp");
-    }
-
-    let base = args.source.parent().unwrap();
-    let iso = args.source.file_stem().unwrap();
-
-    let mut target = base.to_path_buf();
-    target.push(format!("{}-{}.iso", iso.to_str().unwrap(), suffix));
-
-    target.to_path_buf()
-}
-
-fn inject_file_to_iso(iso: &PathBuf, file: &PathBuf, location: &str) -> Result<()> {
-    let result = Command::new("xorriso")
-        .arg("--boot_image")
-        .arg("any")
-        .arg("keep")
-        .arg("-dev")
-        .arg(iso)
-        .arg("-map")
-        .arg(file)
-        .arg(location)
-        .output()?;
-    if !result.status.success() {
-        bail!(
-            "Error injecting {} into {}: {}",
-            file.display(),
-            iso.display(),
-            String::from_utf8(result.stderr)?
-        );
-    }
-    Ok(())
-}
-
-fn get_disks() -> Result<BTreeMap<String, BTreeMap<String, String>>> {
-    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<String, BTreeMap<String, String>> = 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<String, String> = 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)
-}
-
-fn get_nics() -> Result<BTreeMap<String, BTreeMap<String, String>>> {
-    let re_props = Regex::new(r"(?m)^E: (.*)=(.*)$")?;
-    let mut nics: BTreeMap<String, BTreeMap<String, String>> = BTreeMap::new();
-
-    let links = get_nic_list()?;
-    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<String, String> = 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<String> {
-    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)?)
-}
-
-fn parse_answer(path: &PathBuf) -> Result<Answer> {
-    let mut file = match fs::File::open(path) {
-        Ok(file) => file,
-        Err(err) => bail!("Opening answer file '{}' failed: {err}", path.display()),
-    };
-    let mut contents = String::new();
-    if let Err(err) = file.read_to_string(&mut contents) {
-        bail!("Reading from file '{}' failed: {err}", path.display());
-    }
-    match toml::from_str(&contents) {
-        Ok(answer) => {
-            println!("The file was parsed successfully, no syntax errors found!");
-            Ok(answer)
-        }
-        Err(err) => bail!("Error parsing answer file: {err}"),
-    }
-}
-
-fn check_prepare_requirements(args: &CommandPrepareISO) -> Result<()> {
-    match Path::try_exists(&args.source) {
-        Ok(true) => (),
-        Ok(false) => bail!("Source file does not exist."),
-        Err(_) => bail!("Source file does not exist."),
-    }
-
-    match Command::new("xorriso")
-        .arg("-dev")
-        .arg(&args.source)
-        .arg("-find")
-        .arg(PROXMOX_ISO_FLAG)
-        .stderr(Stdio::null())
-        .stdout(Stdio::null())
-        .status()
-    {
-        Ok(v) => {
-            if !v.success() {
-                bail!("The source ISO file is not able to be installed automatically. Please try a more current one.");
-            }
-        }
-        Err(_) => bail!("Could not run 'xorriso'. Please install it."),
-    };
-
-    Ok(())
-}