From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 5CE961FF13B for ; Wed, 25 Feb 2026 16:13:13 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 73BBD6ECC; Wed, 25 Feb 2026 16:14:06 +0100 (CET) Message-ID: Date: Wed, 25 Feb 2026 16:13:56 +0100 MIME-Version: 1.0 User-Agent: Mozilla Thunderbird Subject: Re: [PATCH installer] assistant: add support for splitting ISO into (i)PXE-compatible files To: Christoph Heiss , pve-devel@lists.proxmox.com References: <20260204121025.630269-1-c.heiss@proxmox.com> Content-Language: en-US From: Hannes Duerr In-Reply-To: <20260204121025.630269-1-c.heiss@proxmox.com> Content-Type: text/plain; charset=UTF-8; format=flowed Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1772032420009 X-SPAM-LEVEL: Spam detection results: 0 AWL -1.036 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 1.113 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.358 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.659 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: JHHS3YSTYP67LTRUWGQ3IECDQWKO5TAR X-Message-ID-Hash: JHHS3YSTYP67LTRUWGQ3IECDQWKO5TAR X-MailFrom: h.duerr@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: Please consider this: Reviewed-by: Hannes Dürr Tested-by: Hannes Dürr On 2/4/26 1:10 PM, Christoph Heiss wrote: > 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,