From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 4642A1FF0ED for ; Fri, 31 Jul 2026 16:39:36 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 0A9CB21570; Fri, 31 Jul 2026 16:39:36 +0200 (CEST) From: Christoph Heiss To: pdm-devel@lists.proxmox.com Subject: [PATCH proxmox 02/16] installer-types: post-hook: factor schema version into proper struct Date: Fri, 31 Jul 2026 16:35:25 +0200 Message-ID: <20260731143910.936881-3-c.heiss@proxmox.com> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260731143910.936881-1-c.heiss@proxmox.com> References: <20260731143910.936881-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: 1785508760559 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.000 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_LOW -0.7 Sender listed at https://www.dnswl.org/, low trust 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: XEQAA5HD5T3WODJL4JXGGBRKPINVVVLX X-Message-ID-Hash: XEQAA5HD5T3WODJL4JXGGBRKPINVVVLX 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: This simplifies first and foremost version comparison on consumer sites, making the schema version actually useful. No actual API changes, the version is still serialized to/deserialized from a plain X.Y string. Signed-off-by: Christoph Heiss --- proxmox-installer-types/src/answer.rs | 68 +++++++++++++++++++++--- proxmox-installer-types/src/lib.rs | 18 +++++++ proxmox-installer-types/src/post_hook.rs | 13 +++-- 3 files changed, 90 insertions(+), 9 deletions(-) diff --git a/proxmox-installer-types/src/answer.rs b/proxmox-installer-types/src/answer.rs index 266d6a27..68cac026 100644 --- a/proxmox-installer-types/src/answer.rs +++ b/proxmox-installer-types/src/answer.rs @@ -9,6 +9,7 @@ use anyhow::{Result, anyhow, bail}; use serde::{Deserialize, Serialize}; use std::{ + cmp::Ordering, collections::{BTreeMap, HashMap}, fmt::{self, Display}, str::FromStr, @@ -63,6 +64,38 @@ pub const SUBSCRIPTION_KEY_SCHEMA: proxmox_schema::Schema = .max_length(32) .schema(); +/// Represents a (major, minor) schema version tuple. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct SchemaVersion(pub u32, pub u32); + +impl PartialOrd for SchemaVersion { + fn partial_cmp(&self, other: &Self) -> Option { + match (self.0.cmp(&other.0), self.1.cmp(&other.1)) { + (Ordering::Equal, ord) => Some(ord), + (ord, _) => Some(ord), + } + } +} + +impl Display for SchemaVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}.{}", self.0, self.1) + } +} + +impl FromStr for SchemaVersion { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + crate::parse_version(s) + .map(|(maj, min)| SchemaVersion(maj, min)) + .ok_or_else(|| anyhow!("invalid version")) + } +} + +serde_plain::derive_serialize_from_display!(SchemaVersion); +serde_plain::derive_deserialize_from_fromstr!(SchemaVersion, "valid version"); + /// Defines API types used by proxmox-fetch-answer, the first part of the /// auto-installer. pub mod fetch { @@ -71,9 +104,16 @@ pub mod fetch { #[cfg(feature = "api-types")] use proxmox_schema::api; - use crate::SystemInfo; + use crate::{SystemInfo, answer::SchemaVersion}; - #[cfg_attr(feature = "api-types", api)] + #[cfg_attr(feature = "api-types", api( + properties: { + version: { + type: String, + }, + }, + additional_properties: true, + ))] #[derive(Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] /// Metadata of the HTTP POST payload, such as schema version of the document. @@ -85,17 +125,17 @@ pub mod fetch { /// field. /// minor: Incremented when adding functionality in a backwards-compatible matter, e.g. /// adding a new field. - pub version: String, + pub version: SchemaVersion, } impl AnswerFetchDataSchema { - const SCHEMA_VERSION: &str = "1.0"; + const SCHEMA_VERSION: SchemaVersion = SchemaVersion(1, 0); } impl Default for AnswerFetchDataSchema { fn default() -> Self { Self { - version: Self::SCHEMA_VERSION.to_owned(), + version: Self::SCHEMA_VERSION, } } } @@ -311,7 +351,14 @@ pub enum FqdnSourceMode { FromDhcp, } -#[cfg_attr(feature = "api-types", api)] +#[cfg_attr(feature = "api-types", api( + properties: { + "max-schema-version": { + type: String, + optional: true, + }, + }, +))] #[derive(Clone, Deserialize, Debug, Serialize, PartialEq)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] /// Configuration for the post-installation hook, which runs after an @@ -328,6 +375,15 @@ pub struct PostNotificationHookInfo { #[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "legacy", serde(alias = "auth_token"))] pub auth_token: Option, + + /// Maximum supported schema version by the post-hook target implementation. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_schema_version: Option, + + /// If set, create an API token with the given name and POST its name and secret back with the + /// hook. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub api_token_name: Option, } #[cfg_attr(feature = "api-types", api)] diff --git a/proxmox-installer-types/src/lib.rs b/proxmox-installer-types/src/lib.rs index 8d5ac4fc..718867be 100644 --- a/proxmox-installer-types/src/lib.rs +++ b/proxmox-installer-types/src/lib.rs @@ -179,3 +179,21 @@ impl ProxmoxProduct { } } } + +pub(crate) fn parse_version(s: &str) -> Option<(u32, u32)> { + s.split_once('.') + .and_then(|(maj, min)| Some((maj.parse().ok()?, min.parse().ok()?))) +} + +#[cfg(test)] +mod tests { + use crate::ProductVersion; + + use super::{IsoInfo, parse_version}; + + #[test] + fn parse_version_works() { + assert_eq!(parse_version("42.1"), Some((42, 1))); + assert_eq!(parse_version("a.1"), None); + } +} diff --git a/proxmox-installer-types/src/post_hook.rs b/proxmox-installer-types/src/post_hook.rs index 77cbef48..03a12bdf 100644 --- a/proxmox-installer-types/src/post_hook.rs +++ b/proxmox-installer-types/src/post_hook.rs @@ -8,7 +8,7 @@ use proxmox_schema::api; use crate::{ BootType, IsoInfo, ProxmoxProduct, SystemDMI, UdevProperties, - answer::{FilesystemType, RebootMode}, + answer::{FilesystemType, RebootMode, SchemaVersion}, }; /// Re-export for convenience, since this is public API @@ -137,7 +137,14 @@ pub struct CpuInfo { pub sockets: usize, } -#[cfg_attr(feature = "api-types", api)] +#[cfg_attr(feature = "api-types", api( + properties: { + version: { + type: String, + }, + }, + additional_properties: true, +))] #[derive(Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "kebab-case")] /// Metadata of the hook, such as schema version of the document. @@ -149,7 +156,7 @@ pub struct PostHookInfoSchema { /// field. /// minor: Incremented when adding functionality in a backwards-compatible matter, e.g. /// adding a new field. - pub version: String, + pub version: SchemaVersion, } #[cfg_attr(feature = "api-types", api( -- 2.54.0