From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 9B6AB1FF13F for ; Thu, 12 Mar 2026 12:43:00 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 69E41125A6; Thu, 12 Mar 2026 12:42:55 +0100 (CET) From: Christian Ebner To: pbs-devel@lists.proxmox.com Subject: [PATCH proxmox v3 2/2] pbs-api-types: move over NodeConfig and related api type from PBS Date: Thu, 12 Mar 2026 12:42:02 +0100 Message-ID: <20260312114208.514373-3-c.ebner@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260312114208.514373-1-c.ebner@proxmox.com> References: <20260312114208.514373-1-c.ebner@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1773315703521 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.057 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 RCVD_IN_MSPIKE_H2 0.001 Average reputation (+2) 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: L6LPCZCUDWXKPFCA6TZIAMBVNZZJWHID X-Message-ID-Hash: L6LPCZCUDWXKPFCA6TZIAMBVNZZJWHID X-MailFrom: c.ebner@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 Backup Server development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: In preparation for refactoring the node config in PBS. In order to move over the code, the previously used proxmox_backup::tools::config::from_property_string() helpers are replaced by analogous implementations by inlining the serde_json::from_value() calls. Further, the NodeConfig::validate() was only used by the node's save_config() implementation and has therefore been inlined on the PBS side to avoid a dependency on openssl crate. Signed-off-by: Christian Ebner --- Cargo.toml | 1 + pbs-api-types/Cargo.toml | 3 + pbs-api-types/src/node.rs | 256 +++++++++++++++++++++++++++++++++++++- 3 files changed, 259 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e66ffb78..7ad10ae5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -144,6 +144,7 @@ zstd = "0.13" # workspace dependencies proxmox-access-control = { version = "1.3.0", path = "proxmox-access-control" } +proxmox-acme-api = { version = "1.0.2", path = "proxmox-acme-api" } proxmox-acme = { version = "1.1.0", path = "proxmox-acme", default-features = false } proxmox-api-macro = { version = "1.4.3", path = "proxmox-api-macro" } proxmox-apt = { version = "0.99.6", path = "proxmox-apt" } diff --git a/pbs-api-types/Cargo.toml b/pbs-api-types/Cargo.toml index 4eabe81e..01813f85 100644 --- a/pbs-api-types/Cargo.toml +++ b/pbs-api-types/Cargo.toml @@ -15,10 +15,13 @@ percent-encoding.workspace = true regex.workspace = true serde.workspace = true serde_plain.workspace = true +serde_json.workspace = true +proxmox-acme-api.workspace = true proxmox-auth-api = { workspace = true, features = [ "api-types" ] } proxmox-apt-api-types.workspace = true proxmox-fixed-string.workspace = true +proxmox-http = { workspace = true, features = [ "api-types" ] } proxmox-human-byte.workspace = true proxmox-lang.workspace=true proxmox-s3-client = { workspace = true, features = [ "api-types" ] } diff --git a/pbs-api-types/src/node.rs b/pbs-api-types/src/node.rs index e5b3526c..da3e4118 100644 --- a/pbs-api-types/src/node.rs +++ b/pbs-api-types/src/node.rs @@ -1,13 +1,19 @@ use std::ffi::OsStr; +use anyhow::Error; use serde::{Deserialize, Serialize}; +use proxmox_acme_api::{AcmeConfig, AcmeDomain, ACME_DOMAIN_PROPERTY_SCHEMA}; use proxmox_auth_api::types::Authid; #[cfg(feature = "enum-fallback")] use proxmox_fixed_string::FixedString; +use proxmox_http::{ProxyConfig, HTTP_PROXY_SCHEMA}; use proxmox_schema::*; -use crate::StorageStatus; +use crate::{ + StorageStatus, EMAIL_SCHEMA, MULTI_LINE_COMMENT_SCHEMA, OPENSSL_CIPHERS_TLS_1_2_SCHEMA, + OPENSSL_CIPHERS_TLS_1_3_SCHEMA, +}; #[api] #[derive(Serialize, Deserialize, Default)] @@ -199,3 +205,251 @@ pub struct NodeShellTicket { /// user or authid encoded in the ticket pub user: Authid, } + +/// All available languages in Proxmox. Taken from proxmox-i18n repository. +/// pt_BR, zh_CN, and zh_TW use the same case in the translation files. +// TODO: auto-generate from available translations +#[api] +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Translation { + /// Arabic + Ar, + /// Catalan + Ca, + /// Danish + Da, + /// German + De, + /// English + En, + /// Spanish + Es, + /// Euskera + Eu, + /// Persian (Farsi) + Fa, + /// French + Fr, + /// Galician + Gl, + /// Hebrew + He, + /// Hungarian + Hu, + /// Italian + It, + /// Japanese + Ja, + /// Korean + Kr, + /// Norwegian (Bokmal) + Nb, + /// Dutch + Nl, + /// Norwegian (Nynorsk) + Nn, + /// Polish + Pl, + /// Portuguese (Brazil) + #[serde(rename = "pt_BR")] + PtBr, + /// Russian + Ru, + /// Slovenian + Sl, + /// Swedish + Sv, + /// Turkish + Tr, + /// Chinese (simplified) + #[serde(rename = "zh_CN")] + ZhCn, + /// Chinese (traditional) + #[serde(rename = "zh_TW")] + ZhTw, +} + +#[api( + properties: { + acme: { + optional: true, + type: String, + format: &ApiStringFormat::PropertyString(&AcmeConfig::API_SCHEMA), + }, + acmedomain0: { + schema: ACME_DOMAIN_PROPERTY_SCHEMA, + optional: true, + }, + acmedomain1: { + schema: ACME_DOMAIN_PROPERTY_SCHEMA, + optional: true, + }, + acmedomain2: { + schema: ACME_DOMAIN_PROPERTY_SCHEMA, + optional: true, + }, + acmedomain3: { + schema: ACME_DOMAIN_PROPERTY_SCHEMA, + optional: true, + }, + acmedomain4: { + schema: ACME_DOMAIN_PROPERTY_SCHEMA, + optional: true, + }, + "http-proxy": { + schema: HTTP_PROXY_SCHEMA, + optional: true, + }, + "email-from": { + schema: EMAIL_SCHEMA, + optional: true, + }, + "ciphers-tls-1.3": { + schema: OPENSSL_CIPHERS_TLS_1_3_SCHEMA, + optional: true, + }, + "ciphers-tls-1.2": { + schema: OPENSSL_CIPHERS_TLS_1_2_SCHEMA, + optional: true, + }, + "default-lang" : { + schema: Translation::API_SCHEMA, + optional: true, + }, + "description" : { + optional: true, + schema: MULTI_LINE_COMMENT_SCHEMA, + }, + "consent-text" : { + optional: true, + type: String, + max_length: 64 * 1024, + } + }, +)] +#[derive(Deserialize, Serialize, Updater)] +#[serde(rename_all = "kebab-case")] +/// Node specific configuration. +pub struct NodeConfig { + /// The acme account to use on this node. + #[serde(skip_serializing_if = "Option::is_none")] + pub acme: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub acmedomain0: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub acmedomain1: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub acmedomain2: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub acmedomain3: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub acmedomain4: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub http_proxy: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub email_from: Option, + + /// List of TLS ciphers for TLS 1.3 that will be used by the proxy. (Proxy has to be restarted for changes to take effect) + #[serde(skip_serializing_if = "Option::is_none", rename = "ciphers-tls-1.3")] + pub ciphers_tls_1_3: Option, + + /// List of TLS ciphers for TLS <= 1.2 that will be used by the proxy. (Proxy has to be restarted for changes to take effect) + #[serde(skip_serializing_if = "Option::is_none", rename = "ciphers-tls-1.2")] + pub ciphers_tls_1_2: Option, + + /// Default language used in the GUI + #[serde(skip_serializing_if = "Option::is_none")] + pub default_lang: Option, + + /// Node description + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + + /// Maximum days to keep Task logs + #[serde(skip_serializing_if = "Option::is_none")] + pub task_log_max_days: Option, + + /// Consent banner text + #[serde(skip_serializing_if = "Option::is_none")] + pub consent_text: Option, +} + +impl NodeConfig { + pub fn acme_config(&self) -> Result { + self.acme + .as_deref() + .map(|config| { + Ok(serde_json::from_value( + AcmeConfig::API_SCHEMA.parse_property_string(config)?, + )?) + }) + .unwrap_or_else(|| proxmox_acme_api::parse_acme_config_string("account=default")) + } + + pub fn acme_domains(&'_ self) -> AcmeDomainIter<'_> { + AcmeDomainIter::new(self) + } + + /// Returns the parsed ProxyConfig + pub fn http_proxy(&self) -> Option { + if let Some(http_proxy) = &self.http_proxy { + ProxyConfig::parse_proxy_url(http_proxy).ok() + } else { + None + } + } + + /// Sets the HTTP proxy configuration + pub fn set_http_proxy(&mut self, http_proxy: Option) { + self.http_proxy = http_proxy; + } +} + +pub struct AcmeDomainIter<'a> { + config: &'a NodeConfig, + index: usize, +} + +impl<'a> AcmeDomainIter<'a> { + fn new(config: &'a NodeConfig) -> Self { + Self { config, index: 0 } + } +} + +impl Iterator for AcmeDomainIter<'_> { + type Item = Result; + + fn next(&mut self) -> Option { + let domain = loop { + let index = self.index; + self.index += 1; + + let domain = match index { + 0 => self.config.acmedomain0.as_deref(), + 1 => self.config.acmedomain1.as_deref(), + 2 => self.config.acmedomain2.as_deref(), + 3 => self.config.acmedomain3.as_deref(), + 4 => self.config.acmedomain4.as_deref(), + _ => return None, + }; + + if let Some(domain) = domain { + break domain; + } + }; + + Some( + AcmeDomain::API_SCHEMA + .parse_property_string(domain) + .and_then(|domain| Ok(serde_json::from_value(domain)?)), + ) + } +} -- 2.47.3