public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Christoph Heiss <c.heiss@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH proxmox v2 06/14] installer-types: add types used by the auto-installer
Date: Fri,  5 Dec 2025 12:25:08 +0100	[thread overview]
Message-ID: <20251205112528.373387-7-c.heiss@proxmox.com> (raw)
In-Reply-To: <20251205112528.373387-1-c.heiss@proxmox.com>

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
+}
-- 
2.51.2



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


  parent reply	other threads:[~2025-12-05 11:25 UTC|newest]

Thread overview: 18+ 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-05 11:25 ` [pdm-devel] [PATCH proxmox v2 03/14] network-types: implement api type for Fqdn Christoph Heiss
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-05 11:25 ` [pdm-devel] [PATCH proxmox v2 05/14] installer-types: add common types used by the installer Christoph Heiss
2025-12-05 11:25 ` Christoph Heiss [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-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 08/14] api-types: add api types for auto-installer integration Christoph Heiss
2025-12-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 09/14] config: add auto-installer configuration module 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-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 11/14] server: api: add auto-installer integration module Christoph Heiss
2025-12-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 12/14] ui: auto-installer: add installations overview panel Christoph Heiss
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-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 14/14] docs: add documentation for auto-installer integration Christoph Heiss
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

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=20251205112528.373387-7-c.heiss@proxmox.com \
    --to=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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal