public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Aaron Lauterer <a.lauterer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH installer v5 36/36] autoinst-helper: add prepare-iso subcommand
Date: Tue, 16 Apr 2024 17:33:25 +0200	[thread overview]
Message-ID: <20240416153325.1154224-37-a.lauterer@proxmox.com> (raw)
In-Reply-To: <20240416153325.1154224-1-a.lauterer@proxmox.com>

This new subcommand makes it possible to prepare an ISO to use it for an
automated installation.

It is possible to control the behavior of the resulting automated ISO
with optional parameters.
If no target file is specified, the new ISO will be named with suffixes
to indicate it as automated and additional information. This should help
to distinct between the different options that were chosen to create it.

The code for parsing an answer file is moved to its own function.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
---
 proxmox-autoinst-helper/Cargo.toml  |   1 +
 proxmox-autoinst-helper/src/main.rs | 268 +++++++++++++++++++++++++---
 2 files changed, 247 insertions(+), 22 deletions(-)

diff --git a/proxmox-autoinst-helper/Cargo.toml b/proxmox-autoinst-helper/Cargo.toml
index 2a88c0f..75399e0 100644
--- a/proxmox-autoinst-helper/Cargo.toml
+++ b/proxmox-autoinst-helper/Cargo.toml
@@ -19,3 +19,4 @@ serde_json = "1.0"
 toml = "0.7"
 log = "0.4.20"
 regex = "1.7"
+which = "4.2.5"
diff --git a/proxmox-autoinst-helper/src/main.rs b/proxmox-autoinst-helper/src/main.rs
index fe1cbec..33833ae 100644
--- a/proxmox-autoinst-helper/src/main.rs
+++ b/proxmox-autoinst-helper/src/main.rs
@@ -3,16 +3,29 @@ use clap::{Args, Parser, Subcommand, ValueEnum};
 use glob::Pattern;
 use regex::Regex;
 use serde::Serialize;
-use std::{collections::BTreeMap, fs, io::Read, path::PathBuf, process::Command};
+use std::{
+    collections::BTreeMap,
+    fs,
+    io::Read,
+    path::{Path, PathBuf},
+    process::{Command, Stdio},
+};
+use which::which;
 
 use proxmox_auto_installer::{
     answer::Answer,
     answer::FilterMatch,
     sysinfo,
-    utils::{get_matched_udev_indexes, get_nic_list, get_single_udev_index},
+    utils::{
+        get_matched_udev_indexes, get_nic_list, get_single_udev_index, AutoInstModes,
+        AutoInstSettings,
+    },
 };
 
-/// This tool validates the format of an answer file. Additionally it can test match filters and
+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)]
@@ -23,6 +36,7 @@ struct Cli {
 
 #[derive(Subcommand, Debug)]
 enum Commands {
+    PrepareIso(CommandPrepareISO),
     ValidateAnswer(CommandValidateAnswer),
     DeviceMatch(CommandDeviceMatch),
     DeviceInfo(CommandDeviceInfo),
@@ -76,6 +90,61 @@ struct CommandValidateAnswer {
     debug: bool,
 }
 
+/// Prepare an ISO for automated installation.
+///
+/// In the simplest way, the final ISO will try to fetch an answer file automatically.
+/// It will first search for a partition / file-system called "PROXMOXINST" 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' argument. If not present, it will try to get a
+/// URL from a DHCP option (250, TXT) or as a DNS TXT record located at 'proxmoxinst.{search
+/// domain}'.
+///
+/// The SSL certificate fingerprint can either be defined via the '--ssl_fingerprint' 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 SSL 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 '--ssl_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' parameter.
+/// The answer file can be
+/// * integrated into the ISO itself ('direct')
+/// * needs to be present in a partition / file-system called 'PROXMOXINST' ('partition')
+/// * only be requested via an HTTP Post request ('http').
+#[derive(Args, Debug)]
+#[command(verbatim_doc_comment)]
+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 SSL fingerprint.
+    #[arg(short, long)]
+    ssl_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)]
@@ -116,6 +185,7 @@ struct Devs {
 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),
@@ -188,25 +258,7 @@ fn match_filter(args: &CommandDeviceMatch) -> Result<()> {
 }
 
 fn validate_answer(args: &CommandValidateAnswer) -> Result<()> {
-    let mut file = match fs::File::open(&args.path) {
-        Ok(file) => file,
-        Err(err) => bail!(
-            "Opening answer file '{}' failed: {err}",
-            args.path.display()
-        ),
-    };
-    let mut contents = String::new();
-    if let Err(err) = file.read_to_string(&mut contents) {
-        bail!("Reading from file '{}' failed: {err}", args.path.display());
-    }
-
-    let answer: Answer = match toml::from_str(&contents) {
-        Ok(answer) => {
-            println!("The file was parsed successfully, no syntax errors found!");
-            answer
-        }
-        Err(err) => bail!("Error parsing answer file: {err}"),
-    };
+    let answer = parse_answer(&args.path)?;
     if args.debug {
         println!("Parsed data from answer file:\n{:#?}", answer);
     }
@@ -221,6 +273,128 @@ fn show_identifiers(_args: &CommandIdentifiers) -> Result<()> {
     Ok(())
 }
 
+fn prepare_iso(args: &CommandPrepareISO) -> Result<()> {
+    check_prepare_requirements(args)?;
+
+    if args.install_mode == AutoInstModes::Included && args.answer_file.is_none() {
+        bail!("Missing path to answer file needed for 'direct' install mode.");
+    }
+    if args.install_mode == AutoInstModes::Included && args.ssl_fingerprint.is_some() {
+        bail!("No SSL fingerprint needed for direct install mode. Drop the parameter!");
+    }
+    if args.install_mode == AutoInstModes::Included && args.url.is_some() {
+        bail!("No URL needed for direct 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 args.install_mode == AutoInstModes::Partition && args.ssl_fingerprint.is_some() {
+        bail!("No SSL fingerprint needed for partition install mode. Drop the parameter!");
+    }
+    if args.install_mode == AutoInstModes::Partition && args.url.is_some() {
+        bail!("No URL needed for partition install mode. Drop the parameter!");
+    }
+
+    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(),
+        ssl_fingerprint: args.ssl_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 => "automated".into(),
+        AutoInstModes::Http => "automated-http".into(),
+        AutoInstModes::Included => "automated-answer-included".into(),
+        AutoInstModes::Partition => "automated-part".into(),
+    };
+
+    if args.url.is_some() {
+        suffix.push_str("-url");
+    }
+    if args.ssl_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]*",
@@ -335,3 +509,53 @@ fn get_udev_properties(path: &PathBuf) -> Result<String> {
     }
     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 which("xorriso") {
+        Ok(_) => (),
+        Err(_) => bail!("Could not find 'xorriso'. Please install it and try again"),
+    }
+
+    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(())
+}
-- 
2.39.2





  parent reply	other threads:[~2024-04-16 15:33 UTC|newest]

Thread overview: 41+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2024-04-16 15:32 [pve-devel] [PATCH installer v5 00/36] add automated/unattended installation Aaron Lauterer
2024-04-16 15:32 ` [pve-devel] [PATCH installer v5 01/36] tui: common: move InstallConfig struct to common crate Aaron Lauterer
2024-04-16 15:32 ` [pve-devel] [PATCH installer v5 02/36] common: make InstallZfsOption members public Aaron Lauterer
2024-04-16 15:32 ` [pve-devel] [PATCH installer v5 03/36] common: tui: use BTreeMap for predictable ordering Aaron Lauterer
2024-04-16 15:32 ` [pve-devel] [PATCH installer v5 04/36] common: utils: add deserializer for CidrAddress Aaron Lauterer
2024-04-16 15:32 ` [pve-devel] [PATCH installer v5 05/36] common: options: add Deserialize trait Aaron Lauterer
2024-04-16 15:32 ` [pve-devel] [PATCH installer v5 06/36] low-level: add dump-udev command Aaron Lauterer
2024-04-16 15:32 ` [pve-devel] [PATCH installer v5 07/36] add auto-installer crate Aaron Lauterer
2024-04-16 15:32 ` [pve-devel] [PATCH installer v5 08/36] auto-installer: add dependencies Aaron Lauterer
2024-04-16 15:32 ` [pve-devel] [PATCH installer v5 09/36] auto-installer: add answer file definition Aaron Lauterer
2024-04-16 15:32 ` [pve-devel] [PATCH installer v5 10/36] auto-installer: add struct to hold udev info Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 11/36] auto-installer: add utils Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 12/36] auto-installer: add simple logging Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 13/36] auto-installer: add tests for answer file parsing Aaron Lauterer
2024-04-16 15:36   ` [pve-devel] [PATCH installer v5 13/36, follow-up] " Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 14/36] auto-installer: add auto-installer binary Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 15/36] auto-installer: add fetch answer binary Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 16/36] unconfigured: add proxauto as option to start auto installer Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 17/36] auto-installer: use glob crate for pattern matching Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 18/36] auto-installer: utils: make get_udev_index functions public Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 19/36] auto-installer: add proxmox-autoinst-helper tool Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 20/36] common: add Display trait to ProxmoxProduct Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 21/36] auto-installer: fetch: add gathering of system identifiers and restructure code Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 22/36] auto-installer: helper: add subcommand to view indentifiers Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 23/36] auto-installer: fetch: add http post utility module Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 24/36] auto-installer: fetch: add http plugin to fetch answer Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 25/36] control: update build depends for auto installer Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 26/36] auto installer: factor out fetch-answer and autoinst-helper Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 27/36] low-level: write low level config to /tmp Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 28/36] common: add deserializer for FsType Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 29/36] common: skip target_hd when deserializing InstallConfig Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 30/36] add proxmox-chroot utility Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 31/36] auto-installer: answer: deny unknown fields Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 32/36] fetch-answer: move get_answer_file to utils Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 33/36] auto-installer: utils: define ISO specified settings Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 34/36] fetch-answer: use ISO specified configurations Aaron Lauterer
2024-04-16 15:33 ` [pve-devel] [PATCH installer v5 35/36] fetch-answer: dpcp: improve logging of steps taken Aaron Lauterer
2024-04-16 15:33 ` Aaron Lauterer [this message]
2024-04-17  5:22 ` [pve-devel] [PATCH installer v5 00/36] add automated/unattended installation Thomas Lamprecht
2024-04-17  7:30   ` Aaron Lauterer
2024-04-17 12: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=20240416153325.1154224-37-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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal