From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 5CCC31FF17A for ; Tue, 9 Dec 2025 10:44:18 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 43F9121205; Tue, 9 Dec 2025 10:44:57 +0100 (CET) Mime-Version: 1.0 Date: Tue, 09 Dec 2025 10:44:16 +0100 Message-Id: From: "Lukas Wagner" To: "Proxmox Datacenter Manager development discussion" , "Christoph Heiss" X-Mailer: aerc 0.21.0-0-g5549850facc2-dirty References: <20251205112528.373387-1-c.heiss@proxmox.com> <20251205112528.373387-7-c.heiss@proxmox.com> In-Reply-To: <20251205112528.373387-7-c.heiss@proxmox.com> X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1765273450744 X-SPAM-LEVEL: Spam detection results: 0 AWL -2.470 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_SOMETLD_ARE_BAD_TLD 5 .bar, .beauty, .buzz, .cam, .casa, .cfd, .club, .date, .guru, .link, .live, .monster, .online, .press, .pw, .quest, .rest, .sbs, .shop, .stream, .top, .trade, .wiki, .work, .xyz TLD abuse RCVD_IN_VALIDITY_CERTIFIED_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_RPBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_SAFE_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [lib.rs] Subject: Re: [pdm-devel] [PATCH proxmox v2 06/14] installer-types: add types used by the auto-installer X-BeenThere: pdm-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox Datacenter Manager development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pdm-devel-bounces@lists.proxmox.com Sender: "pdm-devel" 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 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 > --- > 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 , > rustc:native (>= 1.82) , > libstd-rust-dev , > + 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 , > @@ -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, > + #[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, > +} > + > +#[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, > + #[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, > + #[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, > +} > + > +#[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>(fqdn: S) -> Result { > + Ok(Self::Simple( > + fqdn.into() > + .parse::() > + .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) -> 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, > +} > + > +#[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, > +} > + > +#[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, > + /// 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, > +} > + > +/// 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, > +} > + > +#[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, > + /// 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, > +} > + > +#[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, > +} > + > +#[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), > + /// Select disks dynamically by filtering them by udev properties. > + Filter(BTreeMap), > +} > + > +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, > + #[serde(default)] > + /// Filter against udev properties to select the disks for the installation, > + /// to allow dynamic selection of disks. > + pub filter: BTreeMap, > + #[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, > + #[serde(skip_serializing_if = "Option::is_none")] > + /// ZFS-specific filesystem options. > + pub zfs: Option, > + #[serde(skip_serializing_if = "Option::is_none")] > + /// LVM-specific filesystem options, when using ext4 or xfs as filesystem. > + pub lvm: Option, > + #[serde(skip_serializing_if = "Option::is_none")] > + /// Btrfs-specific filesystem options. > + pub btrfs: Option, > +} > + > +impl DiskSetup { > + /// Returns the concrete disk selection made in the setup. > + pub fn disk_selection(&self) -> Result { > + 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, > + #[serde(skip_serializing_if = "Option::is_none")] > + /// `ashift` value to create the zpool with. > + pub ashift: Option, > + #[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, > + #[serde(skip_serializing_if = "Option::is_none")] > + /// Checksumming algorithm to create the zpool with. > + pub checksum: Option, > + #[serde(skip_serializing_if = "Option::is_none")] > + /// Compression algorithm to set on the zpool. > + pub compress: Option, > + #[serde(skip_serializing_if = "Option::is_none")] > + /// `copies` value to create the zpool with. > + pub copies: Option, > + #[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, > +} > + > +#[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, > + #[serde(skip_serializing_if = "Option::is_none")] > + /// Size of the swap volume. Optional, will be heuristically determined if > + /// unset. > + pub swapsize: Option, > + #[serde(skip_serializing_if = "Option::is_none")] > + /// Maximum size the `root` volume. Optional, will be heuristically determined > + /// if unset. > + pub maxroot: Option, > + #[serde(skip_serializing_if = "Option::is_none")] > + /// Maximum size the `data` volume. Optional, will be heuristically determined > + /// if unset. > + pub maxvz: Option, > + #[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, > +} > + > +#[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, > + #[serde(skip_serializing_if = "Option::is_none")] > + /// RAID level to use. > + pub raid: Option, > + #[serde(skip_serializing_if = "Option::is_none")] > + /// Whether to enable filesystem-level compression and what type. > + pub compress: Option, > +} > + > +#[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(&self, serializer: S) -> Result > + 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 { > + 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, D::Error> > +where > + D: serde::Deserializer<'de>, > +{ > + let val: Option = 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, > + /// 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//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, > + /// All network interfaces detected on the system. > + pub network_interfaces: Vec, > + /// 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