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
next prev 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