* [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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox