From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 58DB41FF138 for ; Wed, 04 Feb 2026 13:10:38 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 93C0212049; Wed, 4 Feb 2026 13:11:08 +0100 (CET) From: Christoph Heiss To: pve-devel@lists.proxmox.com Subject: [PATCH installer] assistant: add support for splitting ISO into (i)PXE-compatible files Date: Wed, 4 Feb 2026 13:09:54 +0100 Message-ID: <20260204121025.630269-1-c.heiss@proxmox.com> X-Mailer: git-send-email 2.52.0 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1770206953867 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.001 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 PROLO_LEO1 0.1 Meta Catches all Leo drug variations so far RCVD_IN_VALIDITY_CERTIFIED_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_RPBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_SAFE_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: PK4MLSTT46COM2O7BRLMZLJWUOZUT6L4 X-Message-ID-Hash: PK4MLSTT46COM2O7BRLMZLJWUOZUT6L4 X-MailFrom: c.heiss@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Adds a new flag to the `prepare-iso` subcommand; `--pxe`. When specified, instead of just creating the usual auto-installation-capable ISO, additionally `vmlinuz` and `initrd.img` will be created, which can be used in combination with the ISO to PXE-boot our installer. The (generated) iPXE configuration gives a good overlook how these files must be used, especially what kernel commandline flags must be additionally given. Tl;dr is: The `vmlinuz` is directly booted and given the `initrd.img` as ramdisk, the ISO file must be overlaid as `/proxmox.iso`. This will cause init to mount & load the rest of the installer from there. The flag also takes an optional argument specifying the specific PXE bootloader to generate a configuration file for. Currently only iPXE is supported for configuration generation. Signed-off-by: Christoph Heiss --- FWIW, I'm also working on PXELINUX support, but that has a few quirks. Tested this with a TFTP server and default iPXE. I.e. copying `boot.ipxe`, `vmlinuz`, `initrd.img` and the `.iso` file to the TFTP server root, and then booting off it using the iPXE commandline using: $ dhcp $ chain --autofree tftp:///boot.ipxe In addition, tested this with OPNsense set up such that it automatically instructs iPXE to load `boot.ipxe` from the set TFTP server, completely automating an installation from booting the machine to being able to log into the finished system. debian/control | 2 + proxmox-auto-install-assistant/Cargo.toml | 2 + proxmox-auto-install-assistant/src/main.rs | 489 +++++++++++++++++++-- proxmox-installer-common/src/cli.rs | 2 +- proxmox-installer-common/src/setup.rs | 11 + 5 files changed, 480 insertions(+), 26 deletions(-) diff --git a/debian/control b/debian/control index 9659924..757db94 100644 --- a/debian/control +++ b/debian/control @@ -13,6 +13,7 @@ Build-Depends: cargo:native, librsvg2-bin, librust-anyhow-1-dev, librust-cursive-0.21+crossterm-backend-dev, + librust-flate2-1.1-dev, librust-glob-0.3-dev, librust-hex-0.4-dev, librust-native-tls-dev, @@ -28,6 +29,7 @@ Build-Depends: cargo:native, librust-sha2-0.10-dev, librust-toml-0.8-dev, librust-ureq-3-dev, + librust-zstd-0.13-dev, libtest-mockmodule-perl, patchelf, perl, diff --git a/proxmox-auto-install-assistant/Cargo.toml b/proxmox-auto-install-assistant/Cargo.toml index eeba42f..9a61fb5 100644 --- a/proxmox-auto-install-assistant/Cargo.toml +++ b/proxmox-auto-install-assistant/Cargo.toml @@ -19,3 +19,5 @@ toml.workspace = true proxmox-sys = { version = "1.0.0", features = [ "crypt" ] } glob = "0.3" +flate2 = "1.1" +zstd = { version = "0.13", default-features = false } diff --git a/proxmox-auto-install-assistant/src/main.rs b/proxmox-auto-install-assistant/src/main.rs index af5b703..ca2a4f9 100644 --- a/proxmox-auto-install-assistant/src/main.rs +++ b/proxmox-auto-install-assistant/src/main.rs @@ -4,12 +4,13 @@ #![forbid(unsafe_code)] -use anyhow::{Context, Result, bail, format_err}; +use anyhow::{Context, Result, anyhow, bail, format_err}; use glob::Pattern; use proxmox_sys::{crypt::verify_crypt_pw, linux::tty::read_password}; use std::{ collections::BTreeMap, - fmt, fs, + fmt, + fs::{self, File}, io::{self, IsTerminal, Read}, path::{Path, PathBuf}, process::{self, Command, Stdio}, @@ -26,7 +27,9 @@ use proxmox_auto_installer::{ verify_locale_settings, verify_network_settings, }, }; -use proxmox_installer_common::{FIRST_BOOT_EXEC_MAX_SIZE, FIRST_BOOT_EXEC_NAME, cli}; +use proxmox_installer_common::{ + FIRST_BOOT_EXEC_MAX_SIZE, FIRST_BOOT_EXEC_NAME, cli, setup::ProxmoxProduct, +}; static PROXMOX_ISO_FLAG: &str = "/auto-installer-capable"; @@ -34,6 +37,13 @@ static PROXMOX_ISO_FLAG: &str = "/auto-installer-capable"; /// [LocaleInfo](`proxmox_installer_common::setup::LocaleInfo`) struct. const LOCALE_INFO: &str = include_str!("../../locale-info.json"); +#[derive(Debug, PartialEq)] +struct CdInfo { + product: ProxmoxProduct, + release: String, + isorelease: String, +} + /// Arguments for the `device-info` command. struct CommandDeviceInfoArgs { /// Device type for which information should be shown. @@ -197,6 +207,25 @@ OPTIONS: } } +#[derive(Copy, Clone, Default)] +enum PxeLoader { + #[default] + None, + Ipxe, +} + +impl FromStr for PxeLoader { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "none" => Ok(PxeLoader::None), + "ipxe" => Ok(PxeLoader::Ipxe), + _ => bail!("unknown PXE loader '{s}'"), + } + } +} + /// Arguments for the `prepare-iso` command. struct CommandPrepareISOArgs { /// Path to the source ISO to prepare. @@ -204,6 +233,7 @@ struct CommandPrepareISOArgs { /// Path to store the final ISO to, defaults to an auto-generated file name depending on mode /// and the same directory as the source file is located in. + /// If '--pxe' is specified, the path must be a directory. output: Option, /// Where the automatic installer should fetch the answer file from. @@ -234,10 +264,20 @@ struct CommandPrepareISOArgs { /// /// Must be appropriately enabled in the answer file. on_first_boot: Option, + + /// Instead of producing an ISO file, generate a 'initrd.img' and 'vmlinuz' file for use with + /// (i)PXE servers. The '--output' option must point to a directory to place these files in. + pxe_loader: Option, } impl cli::Subcommand for CommandPrepareISOArgs { fn parse(args: &mut cli::Arguments) -> Result { + let pxe_loader = match args.opt_value_from_str("--pxe") { + Ok(val) => val, + Err(cli::Error::OptionWithoutAValue(_)) => Some(PxeLoader::default()), + Err(err) => Err(err)?, + }; + Ok(Self { output: args.opt_value_from_str("--output")?, fetch_from: args.value_from_str("--fetch-from")?, @@ -249,7 +289,7 @@ impl cli::Subcommand for CommandPrepareISOArgs { .opt_value_from_str("--partition-label")? .unwrap_or_else(default_partition_label), on_first_boot: args.opt_value_from_str("--on-first-boot")?, - // Needs to be last + pxe_loader, input: args.free_from_str()?, }) } @@ -291,6 +331,7 @@ OPTIONS: --output Path to store the final ISO to, defaults to an auto-generated file name depending on mode and the same directory as the source file is located in. + If '--pxe' is specified, the path must be a directory. --fetch-from Where the automatic installer should fetch the answer file from. @@ -323,6 +364,15 @@ OPTIONS: Must be appropriately enabled in the answer file. + --pxe [] + Instead of producing an ISO file, generate a 'initrd.img' and 'vmlinuz' file for use with + (i)PXE servers. The '--output' option must point to a directory to place these files in. + + is optional. Possible value are 'syslinux', 'ipxe'. If is specified, a + configuration file is produced for the specified loader. + + [default: off] + -h, --help Print this help -V, --version Print version "#, @@ -639,12 +689,19 @@ fn prepare_iso(args: &CommandPrepareISOArgs) -> Result<()> { } } + if args.pxe_loader.is_some() + && let Some(out) = &args.output + && (!fs::exists(out)? || !fs::metadata(out)?.is_dir()) + { + bail!("'--output' must point to a directory when '--pxe' is specified."); + } + if let Some(file) = &args.answer_file { println!("Checking provided answer file..."); parse_answer(file)?; } - let iso_target = final_iso_location(args); + let iso_target = final_iso_location(args)?; let iso_target_file_name = match iso_target.file_name() { None => bail!("no base filename in target ISO path found"), Some(source_file_name) => source_file_name.to_string_lossy(), @@ -653,11 +710,14 @@ fn prepare_iso(args: &CommandPrepareISOArgs) -> Result<()> { let mut tmp_base = PathBuf::new(); match args.tmp.as_ref() { Some(tmp_dir) => tmp_base.push(tmp_dir), + None if args.pxe_loader.is_some() && args.output.is_some() => { + tmp_base.push(args.output.as_ref().unwrap()) + } None => tmp_base.push(iso_target.parent().unwrap()), } let mut tmp_iso = tmp_base.clone(); - tmp_iso.push(format!("{iso_target_file_name}.tmp",)); + tmp_iso.push(format!("{iso_target_file_name}.tmp")); println!("Copying source ISO to temporary location..."); fs::copy(&args.input, &tmp_iso)?; @@ -681,6 +741,7 @@ fn prepare_iso(args: &CommandPrepareISOArgs) -> Result<()> { "/auto-installer-mode.toml", &uuid, )?; + let _ = fs::remove_file(&instmode_file_tmp); if let Some(answer_file) = &args.answer_file { inject_file_to_iso(&tmp_iso, answer_file, "/answer.toml", &uuid)?; @@ -695,38 +756,161 @@ fn prepare_iso(args: &CommandPrepareISOArgs) -> Result<()> { )?; } - println!("Moving prepared ISO to target location..."); - fs::rename(&tmp_iso, &iso_target)?; - println!("Final ISO is available at {iso_target:?}."); + if args.pxe_loader.is_some() { + prepare_pxe_compatible_files(args, &tmp_base, &tmp_iso, &iso_target, &uuid)?; + let _ = fs::remove_file(tmp_iso); + } else { + println!("Moving prepared ISO to target location..."); + fs::rename(&tmp_iso, &iso_target)?; + println!("Final ISO is available at {}.", iso_target.display()); + } Ok(()) } -fn final_iso_location(args: &CommandPrepareISOArgs) -> PathBuf { - if let Some(specified) = args.output.clone() { - return specified; +/// Creates and prepares all files needing for PXE-booting the installer. +/// +/// The general flow here is: +/// 1. Extract the kernel and initrd image to the given target folder +/// 2. Recompress the initrd image from zstd to gzip, as PXE loaders generally +/// only support gzip. +/// 3. Remove the `/boot` directory from the target ISO, to save nearly 100 MiB +/// 4. If a particular (supported) PXE loader was given on the command line, +/// generate a configuration file for it. +/// +/// # Arguments +/// +/// `args` - Original commands given to the `prepare-iso` subcommand +/// `tmp_base` - Directory to use a scratch pad +/// `iso_source` - Source ISO file to extract kernel and initrd from +/// `iso_target` - Target ISO file to create, must be different from 'iso_source' +/// * `iso_uuid` - UUID to set for the target ISO +fn prepare_pxe_compatible_files( + args: &CommandPrepareISOArgs, + tmp_base: &Path, + iso_source: &Path, + iso_target: &Path, + iso_uuid: &str, +) -> Result<()> { + debug_assert_ne!( + iso_source, iso_target, + "source and target ISO files must be different" + ); + + println!("Creating vmlinuz and initrd.img for PXE booting..."); + + let out_dir = match &args.output { + Some(out) => out, + None => &args + .input + .parent() + .ok_or_else(|| anyhow!("valid parent path"))? + .to_path_buf(), + }; + + let cd_info_path = out_dir.join(".cd-info.tmp"); + extract_file_from_iso(iso_source, Path::new("/.cd-info"), &cd_info_path)?; + let cd_info = parse_cd_info(&fs::read_to_string(&cd_info_path)?)?; + + extract_file_from_iso( + iso_source, + Path::new("/boot/linux26"), + &out_dir.join("vmlinuz"), + )?; + + let compressed_initrd = tmp_base.join("initrd.img.zst"); + extract_file_from_iso( + iso_source, + Path::new("/boot/initrd.img"), + &compressed_initrd, + )?; + + // re-compress the initrd from zstd to gzip, as iPXE does not support + // zstd-compressed initrds + { + println!("Recompressing initrd using gzip..."); + + let input = File::open(&compressed_initrd) + .with_context(|| format!("opening {compressed_initrd:?}"))?; + + let output_file = File::create(out_dir.join("initrd.img")) + .with_context(|| format!("opening {out_dir:?}/initrd.img"))?; + let mut output = flate2::write::GzEncoder::new(output_file, flate2::Compression::default()); + + zstd::stream::copy_decode(input, &mut output)?; + output.finish()?; } - let mut suffix: String = match args.fetch_from { - FetchAnswerFrom::Http => "auto-from-http", - FetchAnswerFrom::Iso => "auto-from-iso", - FetchAnswerFrom::Partition => "auto-from-partition", + + let iso_target_file_name = iso_target + .file_name() + .map(|name| name.to_string_lossy()) + .ok_or_else(|| anyhow!("no filename found for ISO target?"))?; + println!("Creating ISO file {:?}...", iso_target_file_name); + + // need to remove the output file if it exists, as xorriso refuses to overwrite it + if fs::exists(iso_target)? { + fs::remove_file(iso_target).context("failed to remove existing target ISO file")?; + } + + // remove the whole /boot folder from the ISO to save some space (nearly 100 MiB), as it is + // unnecessary with PXE + remove_file_from_iso(iso_source, iso_target, iso_uuid, "/boot")?; + + let loader = args.pxe_loader.unwrap_or_default(); + create_pxe_config_file(&loader, &cd_info, &iso_target_file_name, out_dir)?; + + // try to clean up all temporary files + let _ = fs::remove_file(&cd_info_path); + let _ = fs::remove_file(&compressed_initrd); + println!("PXE-compatible files are available in {out_dir:?}."); + + Ok(()) +} + +fn final_iso_location(args: &CommandPrepareISOArgs) -> Result { + if let Some(specified) = args.output.clone() + && args.pxe_loader.is_none() + { + return Ok(specified); + } + + let mut filename = args + .input + .file_stem() + .and_then(|s| s.to_str()) + .ok_or_else(|| anyhow!("input name has no filename?"))? + .to_owned(); + + match args.fetch_from { + FetchAnswerFrom::Http => filename.push_str("-auto-from-http"), + FetchAnswerFrom::Iso => filename.push_str("-auto-from-iso"), + FetchAnswerFrom::Partition => filename.push_str("-auto-from-partition"), } - .into(); if args.url.is_some() { - suffix.push_str("-url"); + filename.push_str("-url"); } if args.cert_fingerprint.is_some() { - suffix.push_str("-fp"); + filename.push_str("-fp"); } - let base = args.input.parent().unwrap(); - let iso = args.input.file_stem().unwrap(); + filename.push_str(".iso"); - let mut target = base.to_path_buf(); - target.push(format!("{}-{}.iso", iso.to_str().unwrap(), suffix)); - - target.to_path_buf() + if args.pxe_loader.is_some() { + let path = args + .output + .as_ref() + .ok_or_else(|| anyhow!("output directory must be specified in PXE mode"))? + .join(filename); + Ok(path) + } else { + let path = args + .input + .parent() + .ok_or_else(|| anyhow!("parent directory of input not found"))? + .join(filename); + Ok(path) + } } fn inject_file_to_iso( @@ -757,6 +941,212 @@ fn inject_file_to_iso( Ok(()) } +/// Extracts a file from the given ISO9660 file. +/// +/// If `file` points a directory inside the ISO, `outpath` must be a directory too. +/// +/// # Arguments +/// +/// * `iso` - Source ISO file to extract from +/// * `file` - Absolute file path inside the ISO file to extract. +/// * `outpath` - Output path to write the extracted file to. +fn extract_file_from_iso(iso: &Path, file: &Path, outpath: &Path) -> Result<()> { + debug_assert!(fs::exists(iso).unwrap_or_default()); + + let result = Command::new("xorriso") + .arg("-osirrox") + .arg("on") + .arg("-indev") + .arg(iso) + .arg("-extract") + .arg(file) + .arg(outpath) + .output()?; + + if !result.status.success() { + bail!( + "Error extracting {file:?} from {iso:?} to {outpath:?}: {}", + String::from_utf8_lossy(&result.stderr) + ); + } + Ok(()) +} + +/// Removes a file from the ISO9660 file. +/// +/// # Arguments +/// +/// * `iso_in` - Source ISO file to remove the file from. +/// * `iso_out` - Target ISO file to create with the given filepath removed, must be different from +/// 'iso_in' +/// * `iso_uuid` - UUID to set for the target ISO. +/// * `path` - File path to remove from the ISO file. +fn remove_file_from_iso( + iso_in: &Path, + iso_out: &Path, + iso_uuid: &str, + path: impl AsRef + fmt::Debug, +) -> Result<()> { + debug_assert_ne!( + iso_in, iso_out, + "source and target ISO files must be different" + ); + + let result = Command::new("xorriso") + .arg("-boot_image") + .arg("any") + .arg("keep") + .arg("-volume_date") + .arg("uuid") + .arg(iso_uuid) + .arg("-indev") + .arg(iso_in) + .arg("-outdev") + .arg(iso_out) + .arg("-rm_r") + .arg(path.as_ref()) + .output()?; + + if !result.status.success() { + bail!( + "Error removing {path:?} from {iso_in:?}: {}", + String::from_utf8_lossy(&result.stderr) + ); + } + Ok(()) +} + +struct PxeBootOption { + /// Unique, short, single-word identifier among all entries + id: &'static str, + /// What to show in parenthesis in the menu select + description: &'static str, + /// Extra parameters to append to the kernel commandline + extra_params: &'static str, +} + +const DEFAULT_KERNEL_PARAMS: &str = "ramdisk_size=16777216 rw quiet"; +const PXE_BOOT_OPTIONS: &[PxeBootOption] = &[ + PxeBootOption { + id: "auto", + description: "Automated", + extra_params: "splash=silent proxmox-start-auto-installer", + }, + PxeBootOption { + id: "gui", + description: "Graphical", + extra_params: "splash=silent", + }, + PxeBootOption { + id: "tui", + description: "Terminal UI", + extra_params: "splash=silent proxmox-tui-mode vga=788", + }, + PxeBootOption { + id: "serial", + description: "Terminal UI, Serial Console", + extra_params: "splash=silent proxmox-tui-mode console=ttyS0,115200", + }, + PxeBootOption { + id: "debug", + description: "Debug Mode", + extra_params: "splash=verbose proxmox-debug vga=788", + }, + PxeBootOption { + id: "debugtui", + description: "Terminal UI, Debug Mode", + extra_params: "splash=verbose proxmox-debug proxmox-tui-mode vga=788", + }, + PxeBootOption { + id: "serialdebug", + description: "Serial Console, Debug Mode", + extra_params: "splash=verbose proxmox-debug proxmox-tui-mode console=ttyS0,115200", + }, +]; + +/// Creates a configuration file for the given PXE bootloader. +/// +/// # Arguments +/// +/// * `loader` - PXE bootloader to generate the configuration for +/// * `cd_info` - Information loaded from the ISO +/// * `iso_filename` - Final name of the ISO file, written to the PXE configuration +/// * `out_dir` - Output path to write the file(s) to +fn create_pxe_config_file( + loader: &PxeLoader, + cd_info: &CdInfo, + iso_filename: &str, + out_dir: &Path, +) -> Result<()> { + debug_assert!(fs::exists(out_dir).unwrap_or_default()); + + let product_name = cd_info.product.full_name(); + + let (filename, contents) = match loader { + PxeLoader::None => return Ok(()), + PxeLoader::Ipxe => { + let default_kernel = + format!("kernel vmlinuz {DEFAULT_KERNEL_PARAMS} initrd=initrd.img"); + + let menu_items = PXE_BOOT_OPTIONS + .iter() + .map(|opt| { + format!( + "item {} Install {product_name} ({})\n", + opt.id, opt.description + ) + }) + .collect::(); + + let menu_options = PXE_BOOT_OPTIONS + .iter() + .map(|opt| { + format!( + r#":{} + echo Loading {product_name} {} Installer ... + {default_kernel} {} + goto load + +"#, + opt.id, opt.description, opt.extra_params + ) + }) + .collect::(); + + let script = format!( + r#"#!ipxe + +dhcp + +menu Welcome to {product_name} {}-{} +{menu_items} +choose --default auto --timeout 10000 target && goto ${{target}} + +{menu_options} +:load +initrd initrd.img +initrd {iso_filename} proxmox.iso +boot +"#, + cd_info.release, cd_info.isorelease + ); + + println!("Creating boot.ipxe for iPXE booting..."); + ("boot.ipxe", script) + } + }; + + let target_path = out_dir.join(filename); + fs::create_dir_all( + target_path + .parent() + .ok_or_else(|| anyhow!("expected parent path"))?, + )?; + + fs::write(target_path, contents)?; + Ok(()) +} + fn get_iso_uuid(iso: impl AsRef) -> Result { let result = Command::new("xorriso") .arg("-dev") @@ -947,3 +1337,52 @@ fn check_prepare_requirements(args: &CommandPrepareISOArgs) -> Result<()> { Ok(()) } + +fn parse_cd_info(raw_cd_info: &str) -> Result { + let mut info = CdInfo { + product: ProxmoxProduct::PVE, + release: String::new(), + isorelease: String::new(), + }; + + for line in raw_cd_info.lines() { + match line.split_once('=') { + Some(("PRODUCT", val)) => info.product = val.trim_matches('\'').parse()?, + Some(("RELEASE", val)) => info.release = val.trim_matches('\'').to_owned(), + Some(("ISORELEASE", val)) => info.isorelease = val.trim_matches('\'').to_owned(), + Some(_) => {} + None if line.is_empty() => {} + _ => bail!("invalid cd-info line: {line}"), + } + } + + Ok(info) +} + +#[cfg(test)] +mod tests { + use super::{CdInfo, parse_cd_info}; + use anyhow::Result; + use proxmox_installer_common::setup::ProxmoxProduct; + + #[test] + fn parse_cdinfo() -> Result<()> { + let s = r#" +PRODUCT='pve' +PRODUCTLONG='Proxmox VE' +RELEASE='42.1' +ISORELEASE='1' +ISONAME='proxmox-ve' +"#; + + assert_eq!( + parse_cd_info(s)?, + CdInfo { + product: ProxmoxProduct::PVE, + release: "42.1".into(), + isorelease: "1".into(), + } + ); + Ok(()) + } +} diff --git a/proxmox-installer-common/src/cli.rs b/proxmox-installer-common/src/cli.rs index e2b4d81..5c7a4e6 100644 --- a/proxmox-installer-common/src/cli.rs +++ b/proxmox-installer-common/src/cli.rs @@ -5,7 +5,7 @@ use std::process; use anyhow::Result; -pub use pico_args::Arguments; +pub use pico_args::{Arguments, Error}; pub trait Subcommand { /// Parses the arguments for this command from an [`pico_args::Arguments`]. diff --git a/proxmox-installer-common/src/setup.rs b/proxmox-installer-common/src/setup.rs index 35949f0..8b1cbfe 100644 --- a/proxmox-installer-common/src/setup.rs +++ b/proxmox-installer-common/src/setup.rs @@ -39,6 +39,15 @@ impl ProxmoxProduct { Self::PDM => "pdm", } } + + pub fn full_name(&self) -> &'static str { + match self { + Self::PVE => "Proxmox Virtual Environment", + Self::PMG => "Proxmox Mail Gateway", + Self::PBS => "Proxmox Backup Server", + Self::PDM => "Proxmox Datacenter Manager", + } + } } impl fmt::Display for ProxmoxProduct { @@ -52,6 +61,8 @@ impl fmt::Display for ProxmoxProduct { } } +serde_plain::derive_fromstr_from_deserialize!(ProxmoxProduct); + #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ProductConfig { pub fullname: String, -- 2.52.0