* [PATCH installer] assistant: add support for splitting ISO into (i)PXE-compatible files
@ 2026-02-04 12:09 Christoph Heiss
0 siblings, 0 replies; only message in thread
From: Christoph Heiss @ 2026-02-04 12:09 UTC (permalink / raw)
To: pve-devel
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 <c.heiss@proxmox.com>
---
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://<ip-of-tftp-server>/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<Self> {
+ 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<PathBuf>,
/// 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<PathBuf>,
+
+ /// 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<PxeLoader>,
}
impl cli::Subcommand for CommandPrepareISOArgs {
fn parse(args: &mut cli::Arguments) -> Result<Self> {
+ 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 <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 <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 [<TYPE>]
+ 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.
+
+ <TYPE> is optional. Possible value are 'syslinux', 'ipxe'. If <TYPE> 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<PathBuf> {
+ 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<Path> + 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::<String>();
+
+ 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::<String>();
+
+ 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<Path>) -> Result<String> {
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<CdInfo> {
+ 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
^ permalink raw reply [flat|nested] only message in thread
only message in thread, other threads:[~2026-02-04 12:10 UTC | newest]
Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-02-04 12:09 [PATCH installer] assistant: add support for splitting ISO into (i)PXE-compatible files Christoph Heiss
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.