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 F235B1FF13E for ; Fri, 03 Apr 2026 18:56:14 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id BE1448479; Fri, 3 Apr 2026 18:56:45 +0200 (CEST) From: Christoph Heiss To: pdm-devel@lists.proxmox.com Subject: [PATCH datacenter-manager v3 15/38] api-types: add api types for auto-installer integration Date: Fri, 3 Apr 2026 18:53:47 +0200 Message-ID: <20260403165437.2166551-16-c.heiss@proxmox.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260403165437.2166551-1-c.heiss@proxmox.com> References: <20260403165437.2166551-1-c.heiss@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1775235303428 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.065 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_SHORT 0.001 Use of a URL Shortener for very short URL SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: PJVR3ZBAZH3TLE6WMDA5VTHBE5BLOBSP X-Message-ID-Hash: PJVR3ZBAZH3TLE6WMDA5VTHBE5BLOBSP X-MailFrom: c.heiss@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: The `Installation` type represents an individual installation done through PDM acting as auto-install server, and `PreparedInstallationConfig` a configuration provided by the user for automatically responding to answer requests based on certain target filters. Signed-off-by: Christoph Heiss --- Changes v2 -> v3: * added answer authentication token types * added template counter support * replaced property strings with proper maps for all filter entries * added #[updater(..)] for some more `PreparedInstallationConfig` fields Changes v1 -> v2: * no changes Cargo.toml | 4 + debian/control | 3 + lib/pdm-api-types/Cargo.toml | 3 + lib/pdm-api-types/src/auto_installer.rs | 415 ++++++++++++++++++++++++ lib/pdm-api-types/src/lib.rs | 2 + 5 files changed, 427 insertions(+) create mode 100644 lib/pdm-api-types/src/auto_installer.rs diff --git a/Cargo.toml b/Cargo.toml index ec2aa3d..77b10af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,8 @@ proxmox-tfa = { version = "6", features = [ "api-types" ], default-features = fa proxmox-time = "2" proxmox-upgrade-checks = "1" proxmox-uuid = "1" +proxmox-installer-types = "0.1" +proxmox-network-types = "1.0" # other proxmox crates proxmox-acme = "1.0" @@ -158,6 +160,7 @@ zstd = { version = "0.13" } # proxmox-http-error = { path = "../proxmox/proxmox-http-error" } # proxmox-http = { path = "../proxmox/proxmox-http" } # proxmox-human-byte = { path = "../proxmox/proxmox-human-byte" } +# proxmox-installer-types = { path = "../proxmox/proxmox-installer-types" } # proxmox-io = { path = "../proxmox/proxmox-io" } # proxmox-lang = { path = "../proxmox/proxmox-lang" } # proxmox-ldap = { path = "../proxmox/proxmox-ldap" } @@ -165,6 +168,7 @@ zstd = { version = "0.13" } # proxmox-log = { path = "../proxmox/proxmox-log" } # proxmox-metrics = { path = "../proxmox/proxmox-metrics" } # proxmox-network-api = { path = "../proxmox/proxmox-network-api" } +# proxmox-network-types = { path = "../proxmox/proxmox-network-types" } # proxmox-node-status = { path = "../proxmox/proxmox-node-status" } # proxmox-notify = { path = "../proxmox/proxmox-notify" } # proxmox-openid = { path = "../proxmox/proxmox-openid" } diff --git a/debian/control b/debian/control index 4ddc9ef..6c9ec38 100644 --- a/debian/control +++ b/debian/control @@ -62,6 +62,8 @@ Build-Depends: debhelper-compat (= 13), librust-proxmox-http-1+proxmox-async-dev (>= 1.0.4-~~), librust-proxmox-http-1+websocket-dev (>= 1.0.4-~~), librust-proxmox-human-byte-1+default-dev, + librust-proxmox-installer-types-0.1+api-types-dev, + librust-proxmox-installer-types-0.1+default-dev, librust-proxmox-lang-1+default-dev (>= 1.1-~~), librust-proxmox-ldap-1+default-dev (>= 1.1-~~), librust-proxmox-ldap-1+sync-dev (>= 1.1-~~), @@ -72,6 +74,7 @@ Build-Depends: debhelper-compat (= 13), librust-proxmox-network-api-1+impl-dev, librust-proxmox-node-status-1+api-dev, librust-proxmox-openid-1+default-dev (>= 1.0.2-~~), + librust-proxmox-network-types-1+default-dev, librust-proxmox-product-config-1+default-dev, librust-proxmox-rest-server-1+default-dev, librust-proxmox-rest-server-1+templates-dev, diff --git a/lib/pdm-api-types/Cargo.toml b/lib/pdm-api-types/Cargo.toml index 7aa7b64..7929504 100644 --- a/lib/pdm-api-types/Cargo.toml +++ b/lib/pdm-api-types/Cargo.toml @@ -19,12 +19,15 @@ proxmox-auth-api = { workspace = true, features = ["api-types"] } proxmox-apt-api-types.workspace = true proxmox-lang.workspace = true proxmox-config-digest.workspace = true +proxmox-installer-types = { workspace = true, features = ["api-types"] } +proxmox-network-types = { workspace = true, features = ["api-types"] } proxmox-schema = { workspace = true, features = ["api-macro"] } proxmox-section-config.workspace = true proxmox-dns-api.workspace = true proxmox-time.workspace = true proxmox-serde.workspace = true proxmox-subscription = { workspace = true, features = ["api-types"], default-features = false } +proxmox-uuid = { workspace = true, features = ["serde"] } pbs-api-types = { workspace = true } pve-api-types = { workspace = true } diff --git a/lib/pdm-api-types/src/auto_installer.rs b/lib/pdm-api-types/src/auto_installer.rs new file mode 100644 index 0000000..fbdc7dc --- /dev/null +++ b/lib/pdm-api-types/src/auto_installer.rs @@ -0,0 +1,415 @@ +//! API types used for the auto-installation configuration. + +use serde::{Deserialize, Serialize}; +use std::{collections::BTreeMap, fmt::Debug}; + +use proxmox_auth_api::types::Userid; +use proxmox_installer_types::{post_hook::PostHookInfo, SystemInfo}; +use proxmox_network_types::ip_address::{api_types::IpAddr, Cidr}; +use proxmox_schema::{ + api, + api_types::{ + CERT_FINGERPRINT_SHA256_SCHEMA, COMMENT_SCHEMA, DISK_ARRAY_SCHEMA, HTTP_URL_SCHEMA, + SINGLE_LINE_COMMENT_FORMAT, UUID_FORMAT, + }, + ApiStringFormat, Schema, StringSchema, Updater, +}; +use proxmox_uuid::Uuid; + +use crate::PROXMOX_TOKEN_NAME_SCHEMA; + +/// Re-export for convenience, as these types are used within [`PreparedInstallationConfig`]. +pub use proxmox_installer_types::answer; + +pub const INSTALLATION_UUID_SCHEMA: Schema = StringSchema::new("UUID of a installation.") + .format(&UUID_FORMAT) + .schema(); + +#[api] +#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "kebab-case")] +/// Current status of an installation. +pub enum InstallationStatus { + /// An appropriate answer file was found and sent to the machine. Post-hook was unavailable, + /// so no further status is received. + AnswerSent, + /// Found no matching answer configuration and no default was set. + NoAnswerFound, + /// The installation is currently underway. + InProgress, + /// The installation was finished successfully. + Finished, +} + +#[api( + properties: { + uuid: { + schema: INSTALLATION_UUID_SCHEMA, + }, + "received-at": { + minimum: 0, + }, + }, +)] +#[derive(Clone, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +/// A installation received from some proxmox-auto-installer instance. +pub struct Installation { + /// Unique ID of this installation. + pub uuid: Uuid, + /// Time the installation request was received (Unix Epoch). + pub received_at: i64, + /// Current status of this installation. + pub status: InstallationStatus, + /// System information about the machine to be provisioned. + pub info: SystemInfo, + /// Answer that was sent to the target machine. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub answer_id: Option, + /// Post-installation notification hook data, if available. + #[serde(skip_serializing_if = "Option::is_none")] + pub post_hook_data: Option, +} + +#[api] +#[derive(Debug, Default, Copy, Clone, PartialEq, Deserialize, Serialize, Updater)] +#[serde(rename_all = "lowercase")] +/// How to select the target installations disks. +pub enum DiskSelectionMode { + #[default] + /// Use the fixed list of disks. + Fixed, + /// Dynamically determine target disks based on udev filters. + Filter, +} + +serde_plain::derive_fromstr_from_deserialize!(DiskSelectionMode); + +pub const PREPARED_INSTALL_CONFIG_ID_SCHEMA: proxmox_schema::Schema = + StringSchema::new("ID of prepared configuration for automated installations.") + .min_length(3) + .max_length(64) + .schema(); + +#[api( + properties: { + id: { + schema: PREPARED_INSTALL_CONFIG_ID_SCHEMA, + }, + "authorized-tokens": { + type: Array, + optional: true, + items: { + schema: PROXMOX_TOKEN_NAME_SCHEMA, + }, + }, + "is-default": { + optional: true, + }, + "target-filter": { + type: Object, + properties: {}, + additional_properties: true, + optional: true, + }, + country: { + format: &ApiStringFormat::Pattern(&answer::COUNTRY_CODE_REGEX), + min_length: 2, + max_length: 2, + }, + mailto: { + min_length: 2, + max_length: 256, + format: &SINGLE_LINE_COMMENT_FORMAT, + }, + "root-ssh-keys": { + type: Array, + optional: true, + items: { + type: String, + description: "SSH public key.", + }, + }, + "netdev-filter": { + type: Object, + properties: {}, + additional_properties: true, + optional: true, + }, + "disk-mode": { + type: String, + }, + "disk-list": { + schema: DISK_ARRAY_SCHEMA, + optional: true, + }, + "disk-filter": { + type: Object, + properties: {}, + additional_properties: true, + optional: true, + }, + "post-hook-base-url": { + schema: HTTP_URL_SCHEMA, + optional: true, + }, + "post-hook-cert-fp": { + schema: CERT_FINGERPRINT_SHA256_SCHEMA, + optional: true, + }, + "template-counters": { + type: Object, + properties: {}, + additional_properties: true, + optional: true, + } + }, +)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Updater)] +#[serde(rename_all = "kebab-case")] +/// Configuration describing an automated installation. +/// +/// Certain fields support simple templating via [Handlebars]. Currently, following fields will +/// resolve handlebars expression upon instantiation of an answer: +/// +/// * `fqdn` +/// * `mailto` +/// * `cidr` +/// * `gateway` +/// * `dns +/// +/// [Handlebars]: https://handlebarsjs.com/guide/ +pub struct PreparedInstallationConfig { + #[updater(skip)] + pub id: String, + + #[updater(serde(default, skip_serializing_if = "Option::is_none"))] + #[serde(default, skip_serializing_if = "Vec::is_empty")] + /// List of token IDs that are authoried to retrieve this answer. + pub authorized_tokens: Vec, + + /// Whether this is the default answer. There can only ever be one default answer. + /// `target_filter` below is ignored if this is `true`. + #[updater(serde(default, skip_serializing_if = "Option::is_none"))] + pub is_default: bool, + + // Target filters + /// Map of filters for matching against a property in [`answer::fetch::AnswerFetchData`]. + /// The keys are JSON Pointers as per [RFC6901], the values globs as accepted + /// by the [glob] crate. + /// + /// Used to check this configuration against incoming automated installation requests. If this + /// is unset, it will match any installation not matched "narrower" by other prepared + /// configurations, thus being the default. + /// + /// [RFC6901] https://datatracker.ietf.org/doc/html/rfc6901 + /// [glob crate] https://docs.rs/glob/ + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + #[updater(serde(default, skip_serializing_if = "Option::is_none"))] + pub target_filter: BTreeMap, + + // Keys from [`answer::GlobalOptions`], adapted to better fit the API and model of the UI. + /// Country to use for apt mirrors. + #[updater(serde(default, skip_serializing_if = "Option::is_none"))] + pub country: String, + /// FQDN to set for the installed system. Only used if `use_dhcp_fqdn` is true. + /// + /// Supports templating via Handlebars. + /// The [`proxmox_network_types::fqdn::Fqdn`] type cannot be used here + /// because of that, as curly brackets are not valid in hostnames. + #[updater(serde(default, skip_serializing_if = "Option::is_none"))] + pub fqdn: String, + /// Whether to use the FQDN from the DHCP lease or the user-provided one. + #[updater(serde(default, skip_serializing_if = "Option::is_none"))] + pub use_dhcp_fqdn: bool, + /// Keyboard layout to set. + #[updater(serde(default, skip_serializing_if = "Option::is_none"))] + pub keyboard: answer::KeyboardLayout, + /// Mail address for `root@pam`. + /// + /// Supports templating via Handlebars. + #[updater(serde(default, skip_serializing_if = "Option::is_none"))] + pub mailto: String, + /// Timezone to set on the new system. + #[updater(serde(default, skip_serializing_if = "Option::is_none"))] + pub timezone: String, + /// Pre-hashed password to set for the `root` PAM account. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[updater(serde(default, skip_serializing_if = "Option::is_none"))] + pub root_password_hashed: Option, + /// Whether to reboot the machine if an error occurred during the + /// installation. + #[updater(serde(default, skip_serializing_if = "Option::is_none"))] + pub reboot_on_error: bool, + /// Action to take after the installation completed successfully. + #[updater(serde(default, skip_serializing_if = "Option::is_none"))] + pub reboot_mode: answer::RebootMode, + /// Newline-separated list of public SSH keys to set up for the `root` PAM account. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[updater(serde(default, skip_serializing_if = "Option::is_none"))] + pub root_ssh_keys: Vec, + + // Keys from [`answer::NetworkConfig`], adapted to better fit the API and model of the UI. + /// Whether to use the network configuration from the DHCP lease or not. + #[updater(serde(default, skip_serializing_if = "Option::is_none"))] + pub use_dhcp_network: bool, + /// IP address and netmask if not using DHCP. + /// + /// Supports templating via Handlebars. + #[serde(skip_serializing_if = "Option::is_none")] + #[updater(serde(skip_serializing_if = "Option::is_none"))] + pub cidr: Option, + /// Gateway if not using DHCP. + /// + /// Supports templating via Handlebars. + #[serde(skip_serializing_if = "Option::is_none")] + #[updater(serde(skip_serializing_if = "Option::is_none"))] + pub gateway: Option, + /// DNS server address if not using DHCP. + /// + /// Supports templating via Handlebars. + #[serde(skip_serializing_if = "Option::is_none")] + #[updater(serde(skip_serializing_if = "Option::is_none"))] + pub dns: Option, + /// Filter for network devices, to select a specific management interface. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + #[updater(serde(default, skip_serializing_if = "Option::is_none"))] + pub netdev_filter: BTreeMap, + /// Whether to enable network interface name pinning. + #[updater(serde(default, skip_serializing_if = "Option::is_none"))] + pub netif_name_pinning_enabled: bool, + + /// Root filesystem options. + #[updater(serde(default, skip_serializing_if = "Option::is_none"))] + pub filesystem: answer::FilesystemOptions, + + /// Whether to use the fixed disk list or select disks dynamically by udev filters. + #[updater(serde(default, skip_serializing_if = "Option::is_none"))] + pub disk_mode: DiskSelectionMode, + /// List of raw disk identifiers to use for the root filesystem. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[updater(serde(skip_serializing_if = "Option::is_none"))] + pub disk_list: Vec, + /// Filter against udev properties to select the disks for the installation, + /// to allow dynamic selection of disks. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + #[updater(serde(default, skip_serializing_if = "Option::is_none"))] + pub disk_filter: BTreeMap, + /// Whether it is enough that any filter matches on a disk or all given + /// filters must match to select a disk. Only used if `disk_list` is unset. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[updater(serde(default, skip_serializing_if = "Option::is_none"))] + pub disk_filter_match: Option, + + /// Post installations hook base URL, i.e. host PDM is reachable as from + /// the target machine. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[updater(serde(default, skip_serializing_if = "Option::is_none"))] + pub post_hook_base_url: Option, + /// Post hook certificate fingerprint, if needed. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[updater(serde(default, skip_serializing_if = "Option::is_none"))] + pub post_hook_cert_fp: Option, + + /// Key-value pairs of (auto-incrementing) counters. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + #[updater(serde(default, skip_serializing_if = "Option::is_none"))] + pub template_counters: BTreeMap, +} + +#[api] +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Deletable property names for [`PreparedInstallationConfig`] +pub enum DeletablePreparedInstallationConfigProperty { + /// Delete all target filters + TargetFilter, + /// Delete all udev property filters for the management network device + NetdevFilter, + /// Delete all udev property filters for disks + DiskFilter, + /// Delete all `root` user public ssh keys. + RootSshKeys, + /// Delete the post-installation notification base url. + PostHookBaseUrl, + /// Delete the post-installation notification certificate fingerprint. + PostHookCertFp, + /// Delete all templating counters. + TemplateCounters, +} + +serde_plain::derive_display_from_serialize!(DeletablePreparedInstallationConfigProperty); +serde_plain::derive_fromstr_from_deserialize!(DeletablePreparedInstallationConfigProperty); + +#[api( + properties: { + id: { + schema: PROXMOX_TOKEN_NAME_SCHEMA, + }, + "created-by": { + type: String, + }, + comment: { + optional: true, + schema: COMMENT_SCHEMA, + }, + enabled: { + type: bool, + optional: true, + default: true, + }, + "expire-at": { + type: Integer, + optional: true, + minimum: 0, + description: "Token expiration date (seconds since epoch). '0' means no expiration date.", + }, + } +)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Updater)] +#[serde(rename_all = "kebab-case")] +/// An auth token for authenticating requests from the automated installer. +pub struct AnswerAuthToken { + #[updater(skip)] + /// Name of the auth token + pub id: String, + #[updater(skip)] + /// Name of the user that created it + pub created_by: Userid, + #[serde(skip_serializing_if = "Option::is_none", default)] + #[updater(serde(skip_serializing_if = "Option::is_none"))] + /// Optional comment + pub comment: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[updater(serde(skip_serializing_if = "Option::is_none"))] + /// Whether this token is enabled + pub enabled: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[updater(serde(skip_serializing_if = "Option::is_none"))] + /// Expiration time of this token, if any + pub expire_at: Option, +} + +impl AnswerAuthToken { + pub fn is_active(&self) -> bool { + self.enabled.unwrap_or(false) + && self + .expire_at + .map(|exp| exp > 0 && exp <= proxmox_time::epoch_i64()) + .unwrap_or(true) + } +} + +#[api] +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Deletable property names for [`AnswerAuthToken`]. +pub enum DeletableAnswerAuthTokenProperty { + /// Delete the comment + Comment, + /// Delete the expiration date + ExpireAt, +} + +serde_plain::derive_display_from_serialize!(DeletableAnswerAuthTokenProperty); +serde_plain::derive_fromstr_from_deserialize!(DeletableAnswerAuthTokenProperty); diff --git a/lib/pdm-api-types/src/lib.rs b/lib/pdm-api-types/src/lib.rs index aea1b5d..b88f868 100644 --- a/lib/pdm-api-types/src/lib.rs +++ b/lib/pdm-api-types/src/lib.rs @@ -100,6 +100,8 @@ pub use proxmox_schema::upid::*; mod openid; pub use openid::*; +pub mod auto_installer; + pub mod firewall; pub mod remotes; -- 2.53.0