[PATCH proxmox v4 2/8] pbs-api-types: move over NodeConfig and related api type from PBS

Christian Ebner
2026-03-27T12:13:17Z

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 <c.ebner@proxmox.com>
Reviewed-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
---
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 9541fe4d..931a5c7c 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<String>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub acmedomain0: Option<String>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub acmedomain1: Option<String>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub acmedomain2: Option<String>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub acmedomain3: Option<String>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub acmedomain4: Option<String>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub http_proxy: Option<String>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub email_from: Option<String>,
+
+ /// 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<String>,
+
+ /// 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<String>,
+
+ /// Default language used in the GUI
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub default_lang: Option<String>,
+
+ /// Node description
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub description: Option<String>,
+
+ /// Maximum days to keep Task logs
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub task_log_max_days: Option<usize>,
+
+ /// Consent banner text
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub consent_text: Option<String>,
+}
+
+impl NodeConfig {
+ pub fn acme_config(&self) -> Result<AcmeConfig, Error> {
+ 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<ProxyConfig> {
+ 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<String>) {
+ 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<AcmeDomain, Error>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ 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