From: "Lukas Wagner" <l.wagner@proxmox.com>
To: "Proxmox Datacenter Manager development discussion"
<pdm-devel@lists.proxmox.com>,
"Christoph Heiss" <c.heiss@proxmox.com>
Subject: Re: [pdm-devel] [PATCH proxmox v2 06/14] installer-types: add types used by the auto-installer
Date: Tue, 09 Dec 2025 10:44:16 +0100 [thread overview]
Message-ID: <DETL7QHIT8JC.25VUU2JHBCXD7@proxmox.com> (raw)
In-Reply-To: <20251205112528.373387-7-c.heiss@proxmox.com>
Mostly skimmed the code, since these were pre-existing type definitions
with not much logic, but looks good from what I can tell.
Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
On Fri Dec 5, 2025 at 12:25 PM CET, Christoph Heiss wrote:
> Moving them over from proxmox-auto-installer and proxmox-post-hook, to
> allow re-use in other places.
>
> The network configuration and disk setup has been restructured slightly,
> making its typing a bit more ergonomic to work with. No functional
> changes though, still parses from/into the same format.
>
> Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
> ---
> Changes v1 -> v2:
> * no changes
>
> proxmox-installer-types/Cargo.toml | 1 +
> proxmox-installer-types/debian/control | 2 +
> proxmox-installer-types/src/answer.rs | 898 +++++++++++++++++++++++
> proxmox-installer-types/src/lib.rs | 3 +
> proxmox-installer-types/src/post_hook.rs | 183 +++++
> 5 files changed, 1087 insertions(+)
> create mode 100644 proxmox-installer-types/src/answer.rs
> create mode 100644 proxmox-installer-types/src/post_hook.rs
>
> diff --git a/proxmox-installer-types/Cargo.toml b/proxmox-installer-types/Cargo.toml
> index 62df7f4e..96413fe1 100644
> --- a/proxmox-installer-types/Cargo.toml
> +++ b/proxmox-installer-types/Cargo.toml
> @@ -12,6 +12,7 @@ exclude.workspace = true
> rust-version.workspace = true
>
> [dependencies]
> +anyhow.workspace = true
> serde = { workspace = true, features = ["derive"] }
> serde_plain.workspace = true
> proxmox-network-types.workspace = true
> diff --git a/proxmox-installer-types/debian/control b/proxmox-installer-types/debian/control
> index 902977af..d208a014 100644
> --- a/proxmox-installer-types/debian/control
> +++ b/proxmox-installer-types/debian/control
> @@ -6,6 +6,7 @@ Build-Depends: debhelper-compat (= 13),
> Build-Depends-Arch: cargo:native <!nocheck>,
> rustc:native (>= 1.82) <!nocheck>,
> libstd-rust-dev <!nocheck>,
> + librust-anyhow-1+default-dev <!nocheck>,
> librust-proxmox-network-types-0.1+api-types-dev <!nocheck>,
> librust-proxmox-network-types-0.1+default-dev <!nocheck>,
> librust-serde-1+default-dev <!nocheck>,
> @@ -23,6 +24,7 @@ Architecture: any
> Multi-Arch: same
> Depends:
> ${misc:Depends},
> + librust-anyhow-1+default-dev,
> librust-proxmox-network-types-0.1+api-types-dev,
> librust-proxmox-network-types-0.1+default-dev,
> librust-serde-1+default-dev,
> diff --git a/proxmox-installer-types/src/answer.rs b/proxmox-installer-types/src/answer.rs
> new file mode 100644
> index 00000000..7129a941
> --- /dev/null
> +++ b/proxmox-installer-types/src/answer.rs
> @@ -0,0 +1,898 @@
> +//! Defines API types for the answer file format used by proxmox-auto-installer.
> +//!
> +//! **NOTE**: New answer file properties must use kebab-case, but should allow
> +//! snake_case for backwards compatibility.
> +//!
> +//! TODO: Remove the snake_case'd variants in a future major version (e.g.
> +//! PVE 10).
> +
> +use anyhow::{anyhow, bail, Result};
> +use serde::{Deserialize, Serialize};
> +use std::{
> + collections::{BTreeMap, HashMap},
> + fmt::{self, Display},
> + str::FromStr,
> +};
> +
> +use proxmox_network_types::{fqdn::Fqdn, ip_address::Cidr};
> +type IpAddr = std::net::IpAddr;
> +
> +/// Defines API types used by proxmox-fetch-answer, the first part of the
> +/// auto-installer.
> +pub mod fetch {
> + use serde::{Deserialize, Serialize};
> +
> + use crate::SystemInfo;
> +
> + #[derive(Deserialize, Serialize)]
> + #[serde(rename_all = "kebab-case")]
> + /// Metadata of the HTTP POST payload, such as schema version of the document.
> + pub struct AnswerFetchDataSchema {
> + /// major.minor version describing the schema version of this document, in a semanticy-version
> + /// way.
> + ///
> + /// major: Incremented for incompatible/breaking API changes, e.g. removing an existing
> + /// field.
> + /// minor: Incremented when adding functionality in a backwards-compatible matter, e.g.
> + /// adding a new field.
> + pub version: String,
> + }
> +
> + impl AnswerFetchDataSchema {
> + const SCHEMA_VERSION: &str = "1.0";
> + }
> +
> + impl Default for AnswerFetchDataSchema {
> + fn default() -> Self {
> + Self {
> + version: Self::SCHEMA_VERSION.to_owned(),
> + }
> + }
> + }
> +
> + #[derive(Deserialize, Serialize)]
> + #[serde(rename_all = "kebab-case")]
> + /// Data sent in the body of POST request when retrieving the answer file via HTTP(S).
> + ///
> + /// NOTE: The format is versioned through `schema.version` (`$schema.version` in the
> + /// resulting JSON), ensure you update it when this struct or any of its members gets modified.
> + pub struct AnswerFetchData {
> + /// Metadata for the answer file fetch payload
> + // This field is prefixed by `$` on purpose, to indicate that it is document metadata and not
> + // part of the actual content itself. (E.g. JSON Schema uses a similar naming scheme)
> + #[serde(rename = "$schema")]
> + pub schema: AnswerFetchDataSchema,
> + /// Information about the running system, flattened into this structure directly.
> + #[serde(flatten)]
> + pub sysinfo: SystemInfo,
> + }
> +}
> +
> +#[derive(Clone, Deserialize, Debug, Serialize, PartialEq)]
> +#[serde(rename_all = "kebab-case", deny_unknown_fields)]
> +/// Top-level answer file structure, describing all possible options for an
> +/// automated installation.
> +pub struct AutoInstallerConfig {
> + /// General target system options for setting up the system in an automated
> + /// installation.
> + pub global: GlobalOptions,
> + /// Network configuration to set up inside the target installation.
> + pub network: NetworkConfig,
> + #[serde(rename = "disk-setup")]
> + /// Disk configuration for the target installation.
> + pub disks: DiskSetup,
> + #[serde(skip_serializing_if = "Option::is_none")]
> + /// Optional webhook to hit after a successful installation with information
> + /// about the provisioned system.
> + pub post_installation_webhook: Option<PostNotificationHookInfo>,
> + #[serde(skip_serializing_if = "Option::is_none")]
> + /// Optional one-time hook to run on the first boot into the newly provisioned
> + /// system.
> + pub first_boot: Option<FirstBootHookInfo>,
> +}
> +
> +#[derive(Clone, Default, Deserialize, Debug, Serialize, PartialEq)]
> +#[serde(rename_all = "kebab-case", deny_unknown_fields)]
> +/// General target system options for setting up the system in an automated
> +/// installation.
> +pub struct GlobalOptions {
> + /// Country to use for apt mirrors.
> + pub country: String,
> + /// FQDN to set for the installed system.
> + pub fqdn: FqdnConfig,
> + /// Keyboard layout to set.
> + pub keyboard: KeyboardLayout,
> + /// Mail address for `root@pam`.
> + pub mailto: String,
> + /// Timezone to set on the new system.
> + pub timezone: String,
> + #[serde(alias = "root_password", skip_serializing_if = "Option::is_none")]
> + /// Password to set for the `root` PAM account in plain text. Mutual
> + /// exclusive with the `root-password-hashed` option.
> + pub root_password: Option<String>,
> + #[cfg_attr(feature = "legacy", serde(alias = "root_password_hashed"))]
> + #[serde(skip_serializing_if = "Option::is_none")]
> + /// Password to set for the `root` PAM account as hash, created using e.g.
> + /// mkpasswd(8). Mutual exclusive with the `root-password` option.
> + pub root_password_hashed: Option<String>,
> + #[serde(default)]
> + #[cfg_attr(feature = "legacy", serde(alias = "reboot_on_error"))]
> + /// Whether to reboot the machine if an error occurred during the
> + /// installation.
> + pub reboot_on_error: bool,
> + #[serde(default)]
> + #[cfg_attr(feature = "legacy", serde(alias = "reboot_mode"))]
> + /// Action to take after the installation completed successfully.
> + pub reboot_mode: RebootMode,
> + #[serde(default)]
> + #[cfg_attr(feature = "legacy", serde(alias = "root_ssh_keys"))]
> + /// Public SSH keys to set up for the `root` PAM account.
> + pub root_ssh_keys: Vec<String>,
> +}
> +
> +#[derive(Copy, Clone, Deserialize, Serialize, Debug, Default, PartialEq, Eq)]
> +#[serde(rename_all = "kebab-case", deny_unknown_fields)]
> +/// Action to take after the installation completed successfully.
> +pub enum RebootMode {
> + #[default]
> + /// Reboot the machine.
> + Reboot,
> + /// Power off and halt the machine.
> + PowerOff,
> +}
> +
> +serde_plain::derive_fromstr_from_deserialize!(RebootMode);
> +
> +#[derive(Clone, Deserialize, Debug, Serialize, PartialEq)]
> +#[serde(
> + untagged,
> + expecting = "either a fully-qualified domain name or extendend configuration for usage with DHCP must be specified"
> +)]
> +/// Allow the user to either set the FQDN of the installation to either some
> +/// fixed value or retrieve it dynamically via e.g.DHCP.
> +pub enum FqdnConfig {
> + /// Sets the FQDN to the exact value.
> + Simple(Fqdn),
> + /// Extended configuration, e.g. to use hostname and domain from DHCP.
> + FromDhcp(FqdnFromDhcpConfig),
> +}
> +
> +impl Default for FqdnConfig {
> + fn default() -> Self {
> + Self::FromDhcp(FqdnFromDhcpConfig::default())
> + }
> +}
> +
> +impl FqdnConfig {
> + /// Constructs a new "simple" FQDN configuration, i.e. a fixed hostname.
> + pub fn simple<S: Into<String>>(fqdn: S) -> Result<Self> {
> + Ok(Self::Simple(
> + fqdn.into()
> + .parse::<Fqdn>()
> + .map_err(|err| anyhow!("{err}"))?,
> + ))
> + }
> +
> + /// Constructs an extended FQDN configuration, in particular instructing the
> + /// auto-installer to use the FQDN from DHCP lease information.
> + pub fn from_dhcp(domain: Option<String>) -> Self {
> + Self::FromDhcp(FqdnFromDhcpConfig {
> + source: FqdnSourceMode::FromDhcp,
> + domain,
> + })
> + }
> +}
> +
> +#[derive(Clone, Default, Deserialize, Debug, Serialize, PartialEq)]
> +#[serde(rename_all = "kebab-case", deny_unknown_fields)]
> +/// Extended configuration for retrieving the FQDN from external sources.
> +pub struct FqdnFromDhcpConfig {
> + /// Source to gather the FQDN from.
> + #[serde(default)]
> + pub source: FqdnSourceMode,
> + /// Domain to use if none is received via DHCP.
> + #[serde(default, deserialize_with = "deserialize_non_empty_string_maybe")]
> + pub domain: Option<String>,
> +}
> +
> +#[derive(Clone, Deserialize, Debug, Default, PartialEq, Serialize)]
> +#[serde(rename_all = "kebab-case", deny_unknown_fields)]
> +/// Describes the source to retrieve the FQDN of the installation.
> +pub enum FqdnSourceMode {
> + #[default]
> + /// Use the FQDN as provided by the DHCP server, if any.
> + FromDhcp,
> +}
> +
> +#[derive(Clone, Deserialize, Debug, Serialize, PartialEq)]
> +#[serde(rename_all = "kebab-case", deny_unknown_fields)]
> +/// Configuration for the post-installation hook, which runs after an
> +/// installation has completed successfully.
> +pub struct PostNotificationHookInfo {
> + /// URL to send a POST request to
> + pub url: String,
> + /// SHA256 cert fingerprint if certificate pinning should be used.
> + #[serde(skip_serializing_if = "Option::is_none", alias = "cert_fingerprint")]
> + pub cert_fingerprint: Option<String>,
> +}
> +
> +#[derive(Clone, Deserialize, Debug, PartialEq, Serialize)]
> +#[serde(rename_all = "kebab-case", deny_unknown_fields)]
> +/// Possible sources for the optional first-boot hook script/executable file.
> +pub enum FirstBootHookSourceMode {
> + /// Fetch the executable file from an URL, specified in the parent.
> + FromUrl,
> + /// The executable file has been baked into the ISO at a known location,
> + /// and should be retrieved from there.
> + FromIso,
> +}
> +
> +#[derive(Clone, Default, Deserialize, Debug, PartialEq, Serialize)]
> +#[serde(rename_all = "kebab-case", deny_unknown_fields)]
> +/// Possible orderings for the `proxmox-first-boot` systemd service.
> +///
> +/// Determines the final value of `Unit.Before` and `Unit.Wants` in the service
> +/// file.
> +// Must be kept in sync with Proxmox::Install::Config and the service files in the
> +// proxmox-first-boot package.
> +pub enum FirstBootHookServiceOrdering {
> + /// Needed for bringing up the network itself, runs before any networking is attempted.
> + BeforeNetwork,
> + /// Network needs to be already online, runs after networking was brought up.
> + NetworkOnline,
> + /// Runs after the system has successfully booted up completely.
> + #[default]
> + FullyUp,
> +}
> +
> +impl FirstBootHookServiceOrdering {
> + /// Maps the enum to the appropriate systemd target name, without the '.target' suffix.
> + pub fn as_systemd_target_name(&self) -> &str {
> + match self {
> + FirstBootHookServiceOrdering::BeforeNetwork => "network-pre",
> + FirstBootHookServiceOrdering::NetworkOnline => "network-online",
> + FirstBootHookServiceOrdering::FullyUp => "multi-user",
> + }
> + }
> +}
> +
> +#[derive(Clone, Deserialize, Debug, Serialize, PartialEq)]
> +#[serde(rename_all = "kebab-case", deny_unknown_fields)]
> +/// Describes from where to fetch the first-boot hook script, either being baked into the ISO or
> +/// from a URL.
> +pub struct FirstBootHookInfo {
> + /// Mode how to retrieve the first-boot executable file, either from an URL or from the ISO if
> + /// it has been baked-in.
> + pub source: FirstBootHookSourceMode,
> + /// Determines the service order when the hook will run on first boot.
> + #[serde(default)]
> + pub ordering: FirstBootHookServiceOrdering,
> + #[serde(skip_serializing_if = "Option::is_none")]
> + /// Retrieve the post-install script from a URL, if source == "from-url".
> + pub url: Option<String>,
> + /// SHA256 cert fingerprint if certificate pinning should be used, if source == "from-url".
> + #[cfg_attr(feature = "legacy", serde(alias = "cert_fingerprint"))]
> + #[serde(skip_serializing_if = "Option::is_none")]
> + pub cert_fingerprint: Option<String>,
> +}
> +
> +/// Options controlling the behaviour of the network interface pinning (by
> +/// creating appropriate systemd.link files) during the installation.
> +#[derive(Clone, Deserialize, Debug, Serialize, PartialEq)]
> +#[serde(rename_all = "kebab-case", deny_unknown_fields)]
> +pub struct NetworkInterfacePinningOptionsAnswer {
> + /// Whether interfaces should be pinned during the installation.
> + pub enabled: bool,
> + /// Maps MAC address to custom name
> + #[serde(default)]
> + pub mapping: HashMap<String, String>,
> +}
> +
> +#[derive(Clone, Deserialize, Debug, Serialize, PartialEq)]
> +#[serde(rename_all = "kebab-case", deny_unknown_fields)]
> +/// Static network configuration given by the user.
> +pub struct NetworkConfigFromAnswer {
> + /// CIDR of the machine.
> + pub cidr: Cidr,
> + /// DNS nameserver host to use.
> + pub dns: IpAddr,
> + /// Gateway to set.
> + pub gateway: IpAddr,
> + #[serde(default)]
> + /// Filter for network devices, to select a specific management interface.
> + pub filter: BTreeMap<String, String>,
> + /// Controls network interface pinning behaviour during installation.
> + /// Off by default. Allowed for both `from-dhcp` and `from-answer` modes.
> + #[serde(default)]
> + pub interface_name_pinning: Option<NetworkInterfacePinningOptionsAnswer>,
> +}
> +
> +#[derive(Clone, Deserialize, Debug, Serialize, PartialEq)]
> +#[serde(rename_all = "kebab-case", deny_unknown_fields)]
> +/// Use the network configuration received from the DHCP server.
> +pub struct NetworkConfigFromDhcp {
> + /// Controls network interface pinning behaviour during installation.
> + /// Off by default. Allowed for both `from-dhcp` and `from-answer` modes.
> + #[serde(default)]
> + pub interface_name_pinning: Option<NetworkInterfacePinningOptionsAnswer>,
> +}
> +
> +#[derive(Clone, Deserialize, Debug, Serialize, PartialEq)]
> +#[serde(rename_all = "kebab-case", deny_unknown_fields, tag = "source")]
> +/// Network configuration to set up inside the target installation.
> +/// It can either be given statically or taken from the DHCP lease.
> +pub enum NetworkConfig {
> + /// Use the configuration from the DHCP lease.
> + FromDhcp(NetworkConfigFromDhcp),
> + /// Static configuration to apply.
> + FromAnswer(NetworkConfigFromAnswer),
> +}
> +
> +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
> +#[serde(rename_all = "kebab-case", tag = "filesystem")]
> +/// Filesystem-specific options to set on the root disk.
> +pub enum FilesystemOptions {
> + /// LVM-specific options, used when the selected filesystem is ext4 or xfs.
> + Lvm(LvmOptions),
> + /// ZFS-specific options.
> + Zfs(ZfsOptions),
> + /// Btrfs-specific options.
> + Btrfs(BtrfsOptions),
> +}
> +
> +#[derive(Clone, Debug, Serialize)]
> +/// Defines the disks to use for the installation. Can either be a fixed list
> +/// of disk names or a dynamic filter list.
> +pub enum DiskSelection {
> + /// Fixed list of disk names to use for the installation.
> + Selection(Vec<String>),
> + /// Select disks dynamically by filtering them by udev properties.
> + Filter(BTreeMap<String, String>),
> +}
> +
> +impl Display for DiskSelection {
> + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
> + match self {
> + Self::Selection(disks) => write!(f, "{}", disks.join(", ")),
> + Self::Filter(map) => write!(
> + f,
> + "{}",
> + map.iter()
> + .fold(String::new(), |acc, (k, v)| format!("{acc}{k}: {v}\n"))
> + .trim_end()
> + ),
> + }
> + }
> +}
> +
> +#[derive(Copy, Clone, Default, Deserialize, Debug, PartialEq, Serialize)]
> +#[serde(rename_all = "lowercase", deny_unknown_fields)]
> +/// Whether the associated filters must all match for a device or if any one
> +/// is enough.
> +pub enum FilterMatch {
> + /// Device must match any filter.
> + #[default]
> + Any,
> + /// Device must match all given filters.
> + All,
> +}
> +
> +serde_plain::derive_fromstr_from_deserialize!(FilterMatch);
> +
> +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
> +#[serde(rename_all = "kebab-case", deny_unknown_fields)]
> +/// Disk configuration for the target installation.
> +pub struct DiskSetup {
> + /// Filesystem to use on the root disk.
> + pub filesystem: Filesystem,
> + #[serde(default)]
> + #[cfg_attr(feature = "legacy", serde(alias = "disk_list"))]
> + /// List of raw disk identifiers to use for the root filesystem.
> + pub disk_list: Vec<String>,
> + #[serde(default)]
> + /// Filter against udev properties to select the disks for the installation,
> + /// to allow dynamic selection of disks.
> + pub filter: BTreeMap<String, String>,
> + #[cfg_attr(feature = "legacy", serde(alias = "filter_match"))]
> + #[serde(skip_serializing_if = "Option::is_none")]
> + /// Set whether it is enough that any filter matches on a disk or all given
> + /// filters must match to select a disk.
> + pub filter_match: Option<FilterMatch>,
> + #[serde(skip_serializing_if = "Option::is_none")]
> + /// ZFS-specific filesystem options.
> + pub zfs: Option<ZfsOptions>,
> + #[serde(skip_serializing_if = "Option::is_none")]
> + /// LVM-specific filesystem options, when using ext4 or xfs as filesystem.
> + pub lvm: Option<LvmOptions>,
> + #[serde(skip_serializing_if = "Option::is_none")]
> + /// Btrfs-specific filesystem options.
> + pub btrfs: Option<BtrfsOptions>,
> +}
> +
> +impl DiskSetup {
> + /// Returns the concrete disk selection made in the setup.
> + pub fn disk_selection(&self) -> Result<DiskSelection> {
> + if self.disk_list.is_empty() && self.filter.is_empty() {
> + bail!("Need either 'disk-list' or 'filter' set");
> + }
> + if !self.disk_list.is_empty() && !self.filter.is_empty() {
> + bail!("Cannot use both, 'disk-list' and 'filter'");
> + }
> +
> + if !self.disk_list.is_empty() {
> + Ok(DiskSelection::Selection(self.disk_list.clone()))
> + } else {
> + Ok(DiskSelection::Filter(self.filter.clone()))
> + }
> + }
> +
> + /// Returns the concrete filesystem type and corresponding options selected
> + /// in the setup.
> + pub fn filesystem_details(&self) -> Result<(FilesystemType, FilesystemOptions)> {
> + let lvm_checks = || -> Result<()> {
> + if self.zfs.is_some() || self.btrfs.is_some() {
> + bail!("make sure only 'lvm' options are set");
> + }
> + if self.disk_list.len() > 1 {
> + bail!("make sure to define only one disk for ext4 and xfs");
> + }
> + Ok(())
> + };
> +
> + match self.filesystem {
> + Filesystem::Xfs => {
> + lvm_checks()?;
> + Ok((
> + FilesystemType::Xfs,
> + FilesystemOptions::Lvm(self.lvm.unwrap_or_default()),
> + ))
> + }
> + Filesystem::Ext4 => {
> + lvm_checks()?;
> + Ok((
> + FilesystemType::Ext4,
> + FilesystemOptions::Lvm(self.lvm.unwrap_or_default()),
> + ))
> + }
> + Filesystem::Zfs => {
> + if self.lvm.is_some() || self.btrfs.is_some() {
> + bail!("make sure only 'zfs' options are set");
> + }
> + match self.zfs {
> + None | Some(ZfsOptions { raid: None, .. }) => {
> + bail!("ZFS raid level 'zfs.raid' must be set");
> + }
> + Some(opts) => Ok((
> + FilesystemType::Zfs(opts.raid.unwrap()),
> + FilesystemOptions::Zfs(opts),
> + )),
> + }
> + }
> + Filesystem::Btrfs => {
> + if self.zfs.is_some() || self.lvm.is_some() {
> + bail!("make sure only 'btrfs' options are set");
> + }
> + match self.btrfs {
> + None | Some(BtrfsOptions { raid: None, .. }) => {
> + bail!("Btrfs raid level 'btrfs.raid' must be set");
> + }
> + Some(opts) => Ok((
> + FilesystemType::Btrfs(opts.raid.unwrap()),
> + FilesystemOptions::Btrfs(opts),
> + )),
> + }
> + }
> + }
> + }
> +}
> +
> +#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
> +#[serde(rename_all = "lowercase", deny_unknown_fields)]
> +/// Available filesystem during installation.
> +pub enum Filesystem {
> + /// Fourth extended filesystem
> + Ext4,
> + /// XFS
> + Xfs,
> + /// ZFS
> + Zfs,
> + /// Btrfs
> + Btrfs,
> +}
> +
> +#[derive(Clone, Copy, Default, Deserialize, Debug, Serialize, PartialEq)]
> +#[serde(rename_all = "kebab-case", deny_unknown_fields)]
> +/// ZFS-specific filesystem options.
> +pub struct ZfsOptions {
> + #[serde(skip_serializing_if = "Option::is_none")]
> + /// RAID level to use.
> + pub raid: Option<ZfsRaidLevel>,
> + #[serde(skip_serializing_if = "Option::is_none")]
> + /// `ashift` value to create the zpool with.
> + pub ashift: Option<u32>,
> + #[cfg_attr(feature = "legacy", serde(alias = "arc_max"))]
> + #[serde(skip_serializing_if = "Option::is_none")]
> + /// Maximum ARC size that ZFS should use, in MiB.
> + pub arc_max: Option<u32>,
> + #[serde(skip_serializing_if = "Option::is_none")]
> + /// Checksumming algorithm to create the zpool with.
> + pub checksum: Option<ZfsChecksumOption>,
> + #[serde(skip_serializing_if = "Option::is_none")]
> + /// Compression algorithm to set on the zpool.
> + pub compress: Option<ZfsCompressOption>,
> + #[serde(skip_serializing_if = "Option::is_none")]
> + /// `copies` value to create the zpool with.
> + pub copies: Option<u32>,
> + #[serde(skip_serializing_if = "Option::is_none")]
> + /// Size of the root disk to use, can be used to reserve free space on the
> + /// hard disk for further partitioning after the installation. Optional,
> + /// will be heuristically determined if unset.
> + pub hdsize: Option<f64>,
> +}
> +
> +#[derive(Clone, Copy, Default, Deserialize, Serialize, Debug, PartialEq)]
> +#[serde(rename_all = "kebab-case", deny_unknown_fields)]
> +/// LVM-specific filesystem options, when using ext4 or xfs as filesystem.
> +pub struct LvmOptions {
> + #[serde(skip_serializing_if = "Option::is_none")]
> + /// Size of the root disk to use, can be used to reserve free space on the
> + /// hard disk for further partitioning after the installation. Optional,
> + /// will be heuristically determined if unset.
> + pub hdsize: Option<f64>,
> + #[serde(skip_serializing_if = "Option::is_none")]
> + /// Size of the swap volume. Optional, will be heuristically determined if
> + /// unset.
> + pub swapsize: Option<f64>,
> + #[serde(skip_serializing_if = "Option::is_none")]
> + /// Maximum size the `root` volume. Optional, will be heuristically determined
> + /// if unset.
> + pub maxroot: Option<f64>,
> + #[serde(skip_serializing_if = "Option::is_none")]
> + /// Maximum size the `data` volume. Optional, will be heuristically determined
> + /// if unset.
> + pub maxvz: Option<f64>,
> + #[serde(skip_serializing_if = "Option::is_none")]
> + /// Minimum amount of free space that should be left in the LVM volume group.
> + /// Optional, will be heuristically determined if unset.
> + pub minfree: Option<f64>,
> +}
> +
> +#[derive(Clone, Copy, Default, Deserialize, Debug, Serialize, PartialEq)]
> +#[serde(rename_all = "kebab-case", deny_unknown_fields)]
> +/// Btrfs-specific filesystem options.
> +pub struct BtrfsOptions {
> + #[serde(skip_serializing_if = "Option::is_none")]
> + /// Size of the root partition. Optional, will be heuristically determined if
> + /// unset.
> + pub hdsize: Option<f64>,
> + #[serde(skip_serializing_if = "Option::is_none")]
> + /// RAID level to use.
> + pub raid: Option<BtrfsRaidLevel>,
> + #[serde(skip_serializing_if = "Option::is_none")]
> + /// Whether to enable filesystem-level compression and what type.
> + pub compress: Option<BtrfsCompressOption>,
> +}
> +
> +#[derive(Copy, Clone, Deserialize, Serialize, Debug, Default, PartialEq)]
> +#[serde(rename_all = "kebab-case", deny_unknown_fields)]
> +/// Keyboard layout of the system.
> +pub enum KeyboardLayout {
> + /// German
> + De,
> + /// Swiss-German
> + DeCh,
> + /// Danish
> + Dk,
> + /// United Kingdom English
> + EnGb,
> + #[default]
> + /// U.S. English
> + EnUs,
> + /// Spanish
> + Es,
> + /// Finnish
> + Fi,
> + /// French
> + Fr,
> + /// Belgium-French
> + FrBe,
> + /// Canada-French
> + FrCa,
> + /// Swiss-French
> + FrCh,
> + /// Hungarian
> + Hu,
> + /// Icelandic
> + Is,
> + /// Italian
> + It,
> + /// Japanese
> + Jp,
> + /// Lithuanian
> + Lt,
> + /// Macedonian
> + Mk,
> + /// Dutch
> + Nl,
> + /// Norwegian
> + No,
> + /// Polish
> + Pl,
> + /// Portuguese
> + Pt,
> + /// Brazil-Portuguese
> + PtBr,
> + /// Swedish
> + Se,
> + /// Slovenian
> + Si,
> + /// Turkish
> + Tr,
> +}
> +
> +impl Display for KeyboardLayout {
> + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
> + match self {
> + Self::Dk => write!(f, "Danish"),
> + Self::De => write!(f, "German"),
> + Self::DeCh => write!(f, "Swiss-German"),
> + Self::EnGb => write!(f, "United Kingdom"),
> + Self::EnUs => write!(f, "U.S. English"),
> + Self::Es => write!(f, "Spanish"),
> + Self::Fi => write!(f, "Finnish"),
> + Self::Fr => write!(f, "French"),
> + Self::FrBe => write!(f, "Belgium-French"),
> + Self::FrCa => write!(f, "Canada-French"),
> + Self::FrCh => write!(f, "Swiss-French"),
> + Self::Hu => write!(f, "Hungarian"),
> + Self::Is => write!(f, "Icelandic"),
> + Self::It => write!(f, "Italian"),
> + Self::Jp => write!(f, "Japanese"),
> + Self::Lt => write!(f, "Lithuanian"),
> + Self::Mk => write!(f, "Macedonian"),
> + Self::Nl => write!(f, "Dutch"),
> + Self::No => write!(f, "Norwegian"),
> + Self::Pl => write!(f, "Polish"),
> + Self::Pt => write!(f, "Portuguese"),
> + Self::PtBr => write!(f, "Brazil-Portuguese"),
> + Self::Si => write!(f, "Slovenian"),
> + Self::Se => write!(f, "Swedish"),
> + Self::Tr => write!(f, "Turkish"),
> + }
> + }
> +}
> +
> +serde_plain::derive_fromstr_from_deserialize!(KeyboardLayout);
> +
> +#[derive(Copy, Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
> +#[serde(rename_all(deserialize = "lowercase", serialize = "UPPERCASE"))]
> +/// Available Btrfs RAID levels.
> +pub enum BtrfsRaidLevel {
> + #[serde(alias = "RAID0")]
> + /// RAID 0, aka. single or striped.
> + Raid0,
> + #[serde(alias = "RAID1")]
> + /// RAID 1, aka. mirror.
> + Raid1,
> + #[serde(alias = "RAID10")]
> + /// RAID 10, combining stripe and mirror.
> + Raid10,
> +}
> +
> +serde_plain::derive_display_from_serialize!(BtrfsRaidLevel);
> +
> +#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
> +#[serde(rename_all = "lowercase")]
> +/// Possible compression algorithms usable with Btrfs. See the accompanying
> +/// mount option in btrfs(5).
> +pub enum BtrfsCompressOption {
> + /// Enable compression, chooses the default algorithm set by Btrfs.
> + On,
> + #[default]
> + /// Disable compression.
> + Off,
> + /// Use zlib for compression.
> + Zlib,
> + /// Use zlo for compression.
> + Lzo,
> + /// Use Zstandard for compression.
> + Zstd,
> +}
> +
> +serde_plain::derive_display_from_serialize!(BtrfsCompressOption);
> +serde_plain::derive_fromstr_from_deserialize!(BtrfsCompressOption);
> +
> +/// List of all available Btrfs compression options.
> +pub const BTRFS_COMPRESS_OPTIONS: &[BtrfsCompressOption] = {
> + use BtrfsCompressOption::*;
> + &[On, Off, Zlib, Lzo, Zstd]
> +};
> +
> +#[derive(Copy, Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
> +#[serde(rename_all = "UPPERCASE")]
> +/// Available ZFS RAID levels.
> +pub enum ZfsRaidLevel {
> + #[serde(alias = "raid0")]
> + /// RAID 0, aka. single or striped.
> + Raid0,
> + #[serde(alias = "raid1")]
> + /// RAID 1, aka. mirror.
> + Raid1,
> + #[serde(alias = "raid10")]
> + /// RAID 10, combining stripe and mirror.
> + Raid10,
> + #[serde(alias = "raidz-1", rename = "RAIDZ-1")]
> + /// ZFS-specific RAID level, provides fault tolerance for one disk.
> + RaidZ,
> + #[serde(alias = "raidz-2", rename = "RAIDZ-2")]
> + /// ZFS-specific RAID level, provides fault tolerance for two disks.
> + RaidZ2,
> + #[serde(alias = "raidz-3", rename = "RAIDZ-3")]
> + /// ZFS-specific RAID level, provides fault tolerance for three disks.
> + RaidZ3,
> +}
> +
> +serde_plain::derive_display_from_serialize!(ZfsRaidLevel);
> +
> +#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
> +#[serde(rename_all = "lowercase")]
> +/// Possible compression algorithms usable with ZFS.
> +pub enum ZfsCompressOption {
> + #[default]
> + /// Enable compression, chooses the default algorithm set by ZFS.
> + On,
> + /// Disable compression.
> + Off,
> + /// Use lzjb for compression.
> + Lzjb,
> + /// Use lz4 for compression.
> + Lz4,
> + /// Use zle for compression.
> + Zle,
> + /// Use gzip for compression.
> + Gzip,
> + /// Use Zstandard for compression.
> + Zstd,
> +}
> +
> +serde_plain::derive_display_from_serialize!(ZfsCompressOption);
> +serde_plain::derive_fromstr_from_deserialize!(ZfsCompressOption);
> +
> +/// List of all available ZFS compression options.
> +pub const ZFS_COMPRESS_OPTIONS: &[ZfsCompressOption] = {
> + use ZfsCompressOption::*;
> + &[On, Off, Lzjb, Lz4, Zle, Gzip, Zstd]
> +};
> +
> +#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
> +#[serde(rename_all = "kebab-case")]
> +/// Possible checksum algorithms usable with ZFS.
> +pub enum ZfsChecksumOption {
> + #[default]
> + /// Enable compression, chooses the default algorithm set by ZFS.
> + On,
> + /// Use Fletcher4 for checksumming.
> + Fletcher4,
> + /// Use SHA256 for checksumming.
> + Sha256,
> +}
> +
> +serde_plain::derive_display_from_serialize!(ZfsChecksumOption);
> +serde_plain::derive_fromstr_from_deserialize!(ZfsChecksumOption);
> +
> +/// List of all available ZFS checksumming options.
> +pub const ZFS_CHECKSUM_OPTIONS: &[ZfsChecksumOption] = {
> + use ZfsChecksumOption::*;
> + &[On, Fletcher4, Sha256]
> +};
> +
> +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
> +/// The filesystem to use for the installation.
> +pub enum FilesystemType {
> + #[default]
> + /// Fourth extended filesystem.
> + Ext4,
> + /// XFS.
> + Xfs,
> + /// ZFS, with a given RAID level.
> + Zfs(ZfsRaidLevel),
> + /// Btrfs, with a given RAID level.
> + Btrfs(BtrfsRaidLevel),
> +}
> +
> +impl FilesystemType {
> + /// Returns whether this filesystem is Btrfs.
> + pub fn is_btrfs(&self) -> bool {
> + matches!(self, FilesystemType::Btrfs(_))
> + }
> +
> + /// Returns true if the filesystem is used on top of LVM, e.g. ext4 or XFS.
> + pub fn is_lvm(&self) -> bool {
> + matches!(self, FilesystemType::Ext4 | FilesystemType::Xfs)
> + }
> +}
> +
> +impl fmt::Display for FilesystemType {
> + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
> + // Values displayed to the user in the installer UI
> + match self {
> + FilesystemType::Ext4 => write!(f, "ext4"),
> + FilesystemType::Xfs => write!(f, "XFS"),
> + FilesystemType::Zfs(level) => write!(f, "ZFS ({level})"),
> + FilesystemType::Btrfs(level) => write!(f, "BTRFS ({level})"),
> + }
> + }
> +}
> +
> +impl Serialize for FilesystemType {
> + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
> + where
> + S: serde::Serializer,
> + {
> + // These values must match exactly what the low-level installer expects
> + let value = match self {
> + // proxinstall::$fssetup
> + FilesystemType::Ext4 => "ext4",
> + FilesystemType::Xfs => "xfs",
> + // proxinstall::get_zfs_raid_setup()
> + FilesystemType::Zfs(level) => &format!("zfs ({level})"),
> + // proxinstall::get_btrfs_raid_setup()
> + FilesystemType::Btrfs(level) => &format!("btrfs ({level})"),
> + };
> +
> + serializer.collect_str(value)
> + }
> +}
> +
> +impl FromStr for FilesystemType {
> + type Err = String;
> +
> + fn from_str(s: &str) -> Result<Self, Self::Err> {
> + match s {
> + "ext4" => Ok(FilesystemType::Ext4),
> + "xfs" => Ok(FilesystemType::Xfs),
> + "zfs (RAID0)" => Ok(FilesystemType::Zfs(ZfsRaidLevel::Raid0)),
> + "zfs (RAID1)" => Ok(FilesystemType::Zfs(ZfsRaidLevel::Raid1)),
> + "zfs (RAID10)" => Ok(FilesystemType::Zfs(ZfsRaidLevel::Raid10)),
> + "zfs (RAIDZ-1)" => Ok(FilesystemType::Zfs(ZfsRaidLevel::RaidZ)),
> + "zfs (RAIDZ-2)" => Ok(FilesystemType::Zfs(ZfsRaidLevel::RaidZ2)),
> + "zfs (RAIDZ-3)" => Ok(FilesystemType::Zfs(ZfsRaidLevel::RaidZ3)),
> + "btrfs (RAID0)" => Ok(FilesystemType::Btrfs(BtrfsRaidLevel::Raid0)),
> + "btrfs (RAID1)" => Ok(FilesystemType::Btrfs(BtrfsRaidLevel::Raid1)),
> + "btrfs (RAID10)" => Ok(FilesystemType::Btrfs(BtrfsRaidLevel::Raid10)),
> + _ => Err(format!("Could not find file system: {s}")),
> + }
> + }
> +}
> +
> +serde_plain::derive_deserialize_from_fromstr!(FilesystemType, "valid filesystem");
> +
> +/// List of all available filesystem types.
> +pub const FILESYSTEM_TYPE_OPTIONS: &[FilesystemType] = {
> + use FilesystemType::*;
> + &[
> + Ext4,
> + Xfs,
> + Zfs(ZfsRaidLevel::Raid0),
> + Zfs(ZfsRaidLevel::Raid1),
> + Zfs(ZfsRaidLevel::Raid10),
> + Zfs(ZfsRaidLevel::RaidZ),
> + Zfs(ZfsRaidLevel::RaidZ2),
> + Zfs(ZfsRaidLevel::RaidZ3),
> + Btrfs(BtrfsRaidLevel::Raid0),
> + Btrfs(BtrfsRaidLevel::Raid1),
> + Btrfs(BtrfsRaidLevel::Raid10),
> + ]
> +};
> +
> +fn deserialize_non_empty_string_maybe<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
> +where
> + D: serde::Deserializer<'de>,
> +{
> + let val: Option<String> = Deserialize::deserialize(deserializer)?;
> +
> + match val {
> + Some(s) if !s.is_empty() => Ok(Some(s)),
> + _ => Ok(None),
> + }
> +}
> diff --git a/proxmox-installer-types/src/lib.rs b/proxmox-installer-types/src/lib.rs
> index 07927cb0..12679bdc 100644
> --- a/proxmox-installer-types/src/lib.rs
> +++ b/proxmox-installer-types/src/lib.rs
> @@ -7,6 +7,9 @@
> #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
> #![deny(unsafe_code, missing_docs)]
>
> +pub mod answer;
> +pub mod post_hook;
> +
> use serde::{Deserialize, Serialize};
> use std::{
> collections::{BTreeMap, HashMap},
> diff --git a/proxmox-installer-types/src/post_hook.rs b/proxmox-installer-types/src/post_hook.rs
> new file mode 100644
> index 00000000..8fbe54f8
> --- /dev/null
> +++ b/proxmox-installer-types/src/post_hook.rs
> @@ -0,0 +1,183 @@
> +//! Defines API types for the proxmox-auto-installer post-installation hook.
> +
> +use serde::{Deserialize, Serialize};
> +
> +use proxmox_network_types::ip_address::Cidr;
> +
> +use crate::{
> + answer::{FilesystemType, RebootMode},
> + BootType, IsoInfo, ProxmoxProduct, SystemDMI, UdevProperties,
> +};
> +
> +#[derive(Clone, Serialize, Deserialize, PartialEq)]
> +/// Information about the system boot status.
> +pub struct BootInfo {
> + /// Whether the system is booted using UEFI or legacy BIOS.
> + pub mode: BootType,
> + /// Whether SecureBoot is enabled for the installation.
> + #[serde(default, skip_serializing_if = "bool_is_false")]
> + secureboot: bool,
> +}
> +
> +#[derive(Clone, Serialize, Deserialize, PartialEq)]
> +/// Holds all the public keys for the different algorithms available.
> +pub struct SshPublicHostKeys {
> + /// ECDSA-based public host key
> + pub ecdsa: String,
> + /// ED25519-based public host key
> + pub ed25519: String,
> + /// RSA-based public host key
> + pub rsa: String,
> +}
> +
> +#[derive(Clone, Serialize, Deserialize, PartialEq)]
> +#[serde(rename_all = "kebab-case")]
> +/// Holds information about a single disk in the system.
> +pub struct DiskInfo {
> + /// Size in bytes
> + pub size: u64,
> + /// Set to true if the disk is used for booting.
> + #[serde(default, skip_serializing_if = "bool_is_false")]
> + pub is_bootdisk: bool,
> + /// Properties about the device as given by udev.
> + pub udev_properties: UdevProperties,
> +}
> +
> +/// Holds information about the management network interface.
> +#[derive(Clone, Serialize, Deserialize, PartialEq)]
> +#[serde(rename_all = "kebab-case")]
> +pub struct NetworkInterfaceInfo {
> + /// Name of the interface
> + name: String,
> + /// MAC address of the interface
> + pub mac: String,
> + /// (Designated) IP address of the interface
> + #[serde(skip_serializing_if = "Option::is_none")]
> + pub address: Option<Cidr>,
> + /// Set to true if the interface is the chosen management interface during
> + /// installation.
> + #[serde(default, skip_serializing_if = "bool_is_false")]
> + pub is_management: bool,
> + /// Set to true if the network interface name was pinned based on the MAC
> + /// address during the installation.
> + #[serde(default, skip_serializing_if = "bool_is_false")]
> + is_pinned: bool,
> + /// Properties about the device as given by udev.
> + pub udev_properties: UdevProperties,
> +}
> +
> +#[derive(Clone, Serialize, Deserialize, PartialEq)]
> +#[serde(rename_all = "kebab-case")]
> +/// Information about the installed product itself.
> +pub struct ProductInfo {
> + /// Full name of the product
> + pub fullname: String,
> + /// Product abbreviation
> + pub short: ProxmoxProduct,
> + /// Version of the installed product
> + pub version: String,
> +}
> +
> +#[derive(Clone, Serialize, Deserialize, PartialEq)]
> +/// The current kernel version.
> +/// Aligns with the format as used by the `/nodes/<node>/status` API of each product.
> +pub struct KernelVersionInformation {
> + /// The systemname/nodename
> + pub sysname: String,
> + /// The kernel release number
> + pub release: String,
> + /// The kernel version
> + pub version: String,
> + /// The machine architecture
> + pub machine: String,
> +}
> +
> +#[derive(Clone, Serialize, Deserialize, PartialEq)]
> +/// Information about the CPU(s) installed in the system
> +pub struct CpuInfo {
> + /// Number of physical CPU cores.
> + pub cores: u32,
> + /// Number of logical CPU cores aka. threads.
> + pub cpus: u32,
> + /// CPU feature flag set as a space-delimited list.
> + pub flags: String,
> + /// Whether hardware-accelerated virtualization is supported.
> + pub hvm: bool,
> + /// Reported model of the CPU(s)
> + pub model: String,
> + /// Number of physical CPU sockets
> + pub sockets: u32,
> +}
> +
> +#[derive(Clone, Serialize, Deserialize, PartialEq)]
> +#[serde(rename_all = "kebab-case")]
> +/// Metadata of the hook, such as schema version of the document.
> +pub struct PostHookInfoSchema {
> + /// major.minor version describing the schema version of this document, in a semanticy-version
> + /// way.
> + ///
> + /// major: Incremented for incompatible/breaking API changes, e.g. removing an existing
> + /// field.
> + /// minor: Incremented when adding functionality in a backwards-compatible matter, e.g.
> + /// adding a new field.
> + pub version: String,
> +}
> +
> +impl PostHookInfoSchema {
> + const SCHEMA_VERSION: &str = "1.2";
> +}
> +
> +impl Default for PostHookInfoSchema {
> + fn default() -> Self {
> + Self {
> + version: Self::SCHEMA_VERSION.to_owned(),
> + }
> + }
> +}
> +
> +#[derive(Clone, Serialize, Deserialize, PartialEq)]
> +#[serde(rename_all = "kebab-case")]
> +/// All data sent as request payload with the post-installation-webhook POST request.
> +///
> +/// NOTE: The format is versioned through `schema.version` (`$schema.version` in the
> +/// resulting JSON), ensure you update it when this struct or any of its members gets modified.
> +pub struct PostHookInfo {
> + // This field is prefixed by `$` on purpose, to indicate that it is document metadata and not
> + // part of the actual content itself. (E.g. JSON Schema uses a similar naming scheme)
> + #[serde(rename = "$schema")]
> + /// Schema version information for this struct instance.
> + pub schema: PostHookInfoSchema,
> + /// major.minor version of Debian as installed, retrieved from /etc/debian_version
> + pub debian_version: String,
> + /// PVE/PMG/PBS/PDM version as reported by `pveversion`, `pmgversion`,
> + /// `proxmox-backup-manager version` or `proxmox-datacenter-manager version`, respectively.
> + pub product: ProductInfo,
> + /// Release information for the ISO used for the installation.
> + pub iso: IsoInfo,
> + /// Installed kernel version
> + pub kernel_version: KernelVersionInformation,
> + /// Describes the boot mode of the machine and the SecureBoot status.
> + pub boot_info: BootInfo,
> + /// Information about the installed CPU(s)
> + pub cpu_info: CpuInfo,
> + /// DMI information about the system
> + pub dmi: SystemDMI,
> + /// Filesystem used for boot disk(s)
> + pub filesystem: FilesystemType,
> + /// Fully qualified domain name of the installed system
> + pub fqdn: String,
> + /// Unique systemd-id128 identifier of the installed system (128-bit, 16 bytes)
> + pub machine_id: String,
> + /// All disks detected on the system.
> + pub disks: Vec<DiskInfo>,
> + /// All network interfaces detected on the system.
> + pub network_interfaces: Vec<NetworkInterfaceInfo>,
> + /// Public parts of SSH host keys of the installed system
> + pub ssh_public_host_keys: SshPublicHostKeys,
> + /// Action to will be performed, i.e. either reboot or power off the machine.
> + pub reboot_mode: RebootMode,
> +}
> +
> +fn bool_is_false(value: &bool) -> bool {
> + !value
> +}
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
next prev parent reply other threads:[~2025-12-09 9:44 UTC|newest]
Thread overview: 35+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-12-05 11:25 [pdm-devel] [PATCH proxmox/datacenter-manager v2 00/14] initial auto-installer integration Christoph Heiss
2025-12-05 11:25 ` [pdm-devel] [PATCH proxmox v2 01/14] api-macro: allow $ in identifier name Christoph Heiss
2025-12-05 11:25 ` [pdm-devel] [PATCH proxmox v2 02/14] network-types: move `Fqdn` type from proxmox-installer-common Christoph Heiss
2025-12-09 9:13 ` Lukas Wagner
2025-12-09 12:26 ` Christoph Heiss
2025-12-05 11:25 ` [pdm-devel] [PATCH proxmox v2 03/14] network-types: implement api type for Fqdn Christoph Heiss
2025-12-09 9:13 ` Lukas Wagner
2025-12-05 11:25 ` [pdm-devel] [PATCH proxmox v2 04/14] network-types: add api wrapper type for std::net::IpAddr Christoph Heiss
2025-12-09 9:16 ` Lukas Wagner
2025-12-05 11:25 ` [pdm-devel] [PATCH proxmox v2 05/14] installer-types: add common types used by the installer Christoph Heiss
2025-12-09 9:35 ` Lukas Wagner
2025-12-09 12:17 ` Christoph Heiss
2025-12-05 11:25 ` [pdm-devel] [PATCH proxmox v2 06/14] installer-types: add types used by the auto-installer Christoph Heiss
2025-12-09 9:44 ` Lukas Wagner [this message]
2025-12-05 11:25 ` [pdm-devel] [PATCH proxmox v2 07/14] installer-types: implement api type for all externally-used types Christoph Heiss
2025-12-09 9:52 ` Lukas Wagner
2025-12-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 08/14] api-types: add api types for auto-installer integration Christoph Heiss
2025-12-09 10:03 ` Lukas Wagner
2025-12-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 09/14] config: add auto-installer configuration module Christoph Heiss
2025-12-09 10:22 ` Lukas Wagner
2025-12-09 12:10 ` Christoph Heiss
2025-12-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 10/14] acl: wire up new /system/auto-installation acl path Christoph Heiss
2025-12-09 10:23 ` Lukas Wagner
2025-12-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 11/14] server: api: add auto-installer integration module Christoph Heiss
2025-12-09 11:01 ` Lukas Wagner
2025-12-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 12/14] ui: auto-installer: add installations overview panel Christoph Heiss
2025-12-09 12:35 ` Lukas Wagner
2025-12-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 13/14] ui: auto-installer: add prepared answer configuration panel Christoph Heiss
2025-12-09 13:01 ` Lukas Wagner
2025-12-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 14/14] docs: add documentation for auto-installer integration Christoph Heiss
2025-12-09 13:12 ` Lukas Wagner
2025-12-05 11:53 ` [pdm-devel] [PATCH proxmox/datacenter-manager v2 00/14] initial " Thomas Lamprecht
2025-12-05 15:50 ` Christoph Heiss
2025-12-05 15:57 ` Thomas Lamprecht
2025-12-09 13:38 ` Lukas Wagner
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=DETL7QHIT8JC.25VUU2JHBCXD7@proxmox.com \
--to=l.wagner@proxmox.com \
--cc=c.heiss@proxmox.com \
--cc=pdm-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox