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 1BAC91FF142 for ; Mon, 16 Feb 2026 11:46:09 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 2CA17DF4D; Mon, 16 Feb 2026 11:45:23 +0100 (CET) From: Dietmar Maurer To: pve-devel@lists.proxmox.com Subject: [RFC proxmox 18/22] firewall-api-types: add FirewallRuleUpdater type Date: Mon, 16 Feb 2026 11:43:56 +0100 Message-ID: <20260216104401.3959270-19-dietmar@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260216104401.3959270-1-dietmar@proxmox.com> References: <20260216104401.3959270-1-dietmar@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.564 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_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods RDNS_NONE 0.793 Delivered to internal network by a host with no rDNS SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_NONE 0.001 SPF: sender does not publish an SPF Record Message-ID-Hash: SCUALOQ7DEK6O6CHVTBEPMF273NGHQOP X-Message-ID-Hash: SCUALOQ7DEK6O6CHVTBEPMF273NGHQOP X-MailFrom: dietmar@zilli.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 VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: This derives the Updater trait for FirewallRule, which generates a FirewallRuleUpdater struct for use in update APIs. The updater makes all fields optional, allowing partial updates. Added test_updater_type() to verify that all updater fields have the correct types and can be properly initialized. Signed-off-by: Dietmar Maurer --- proxmox-firewall-api-types/src/address.rs | 6 +++- proxmox-firewall-api-types/src/icmp_type.rs | 6 +++- proxmox-firewall-api-types/src/port.rs | 4 +++ proxmox-firewall-api-types/src/rule.rs | 39 +++++++++++++++++++-- 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/proxmox-firewall-api-types/src/address.rs b/proxmox-firewall-api-types/src/address.rs index 46166352..196222ba 100644 --- a/proxmox-firewall-api-types/src/address.rs +++ b/proxmox-firewall-api-types/src/address.rs @@ -5,7 +5,7 @@ use super::{FirewallAliasReference, FirewallIpsetReference}; use anyhow::{bail, Error}; use proxmox_network_types::ip_address::{Cidr, Family, IpRange}; -use proxmox_schema::{ApiStringFormat, ApiType, Schema, StringSchema}; +use proxmox_schema::{ApiStringFormat, ApiType, Schema, StringSchema, UpdaterType}; /// A match for source or destination address. #[derive(Clone, Debug, PartialEq)] @@ -32,6 +32,10 @@ impl ApiType for FirewallAddressMatch { .schema(); } +impl UpdaterType for FirewallAddressMatch { + type Updater = Option; +} + fn verify_firewall_address_match(s: &str) -> Result<(), Error> { FirewallAddressMatch::from_str(s).map(|_| ()) } diff --git a/proxmox-firewall-api-types/src/icmp_type.rs b/proxmox-firewall-api-types/src/icmp_type.rs index b45c1505..46ddb58a 100644 --- a/proxmox-firewall-api-types/src/icmp_type.rs +++ b/proxmox-firewall-api-types/src/icmp_type.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "enum-fallback")] use proxmox_fixed_string::FixedString; -use proxmox_schema::{ApiStringFormat, ApiType, Schema, StringSchema}; +use proxmox_schema::{ApiStringFormat, ApiType, Schema, StringSchema, UpdaterType}; #[derive(Debug, Copy, Clone, PartialEq)] /// ICMP type, either named or numeric. @@ -26,6 +26,10 @@ impl ApiType for FirewallIcmpType { .schema(); } +impl UpdaterType for FirewallIcmpType { + type Updater = Option; +} + fn verify_firewall_icmp_type(value: &str) -> Result<(), Error> { value.parse::().map(|_| ()) } diff --git a/proxmox-firewall-api-types/src/port.rs b/proxmox-firewall-api-types/src/port.rs index 46989ba4..572cb439 100644 --- a/proxmox-firewall-api-types/src/port.rs +++ b/proxmox-firewall-api-types/src/port.rs @@ -84,6 +84,10 @@ pub const FIREWALL_DPORT_API_SCHEMA: Schema = StringSchema::new(concatcp!( .format(&ApiStringFormat::VerifyFn(verify_firewall_port_list)) .schema(); +impl UpdaterType for FirewallPortList { + type Updater = Option; +} + serde_plain::derive_deserialize_from_fromstr!(FirewallPortList, "valid port list"); serde_plain::derive_serialize_from_display!(FirewallPortList); diff --git a/proxmox-firewall-api-types/src/rule.rs b/proxmox-firewall-api-types/src/rule.rs index 8588ca46..48869b25 100644 --- a/proxmox-firewall-api-types/src/rule.rs +++ b/proxmox-firewall-api-types/src/rule.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use proxmox_fixed_string::FixedString; use proxmox_schema::api_types::COMMENT_SCHEMA; -use proxmox_schema::{api, const_regex, ApiStringFormat}; +use proxmox_schema::{api, const_regex, ApiStringFormat, Updater}; use crate::{FirewallAddressMatch, FirewallIcmpType, FirewallPortList}; use crate::{FIREWALL_DPORT_API_SCHEMA, FIREWALL_SPORT_API_SCHEMA}; @@ -84,9 +84,10 @@ const_regex! { }, )] /// Firewall Rule. -#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[derive(Debug, serde::Deserialize, serde::Serialize, Updater)] pub struct FirewallRule { /// Rule action ('ACCEPT', 'DROP', 'REJECT') or security group name. + #[updater(serde(default, skip_serializing_if = "Option::is_none"))] pub action: String, /// Descriptive comment. @@ -99,6 +100,7 @@ pub struct FirewallRule { /// Prevent changes if current configuration file has a different digest. /// This can be used to prevent concurrent modifications. #[serde(default, skip_serializing_if = "Option::is_none")] + #[updater(type = "Option")] pub digest: Option, /// Restrict TCP/UDP destination port. @@ -148,6 +150,7 @@ pub struct FirewallRule { pub sport: Option, #[serde(rename = "type")] + #[updater(serde(default, skip_serializing_if = "Option::is_none"))] pub ty: FirewallRuleType, } @@ -179,6 +182,7 @@ serde_plain::derive_fromstr_from_deserialize!(FirewallRuleType); #[cfg(test)] mod test { use super::*; + use crate::FirewallIcmpTypeName; #[test] fn test_regex_compilation_firewall_rule_iface_re() { @@ -190,4 +194,35 @@ mod test { use regex::Regex; let _: &Regex = &FIREWALL_SECURITY_GROUP_RE; } + + #[test] + fn test_updater_type() { + // Test that we have all properties with correct types + let mut updater = FirewallRuleUpdater::default(); + + // Basic String fields - these are Option in updater + updater.action = Some("ACCEPT".to_string()); + updater.comment = Some("test comment".to_string()); + updater.iface = Some("net0".to_string()); + updater.r#macro = Some("test-macro".to_string()); + updater.proto = Some("tcp".to_string()); + + // Numeric fields - these are Option in updater + updater.enable = Some(1); + updater.pos = Some(0); + + // Enum fields - these are Option in updater + updater.ty = Some(FirewallRuleType::In); + updater.log = Some(FirewallLogLevel::Info); + + // Fields with custom updater types #[updater(type = "Option")] + // These are just Option in the updater (not Option>) + // The #[updater(type)] attribute explicitly sets the updater field type + updater.dest = Some("192.168.1.1".parse().unwrap()); + updater.source = Some("10.0.0.1".parse().unwrap()); + updater.dport = Some("80".parse().unwrap()); + updater.sport = Some("1024:65535".parse().unwrap()); + updater.icmp_type = Some(FirewallIcmpType::Named(FirewallIcmpTypeName::EchoRequest)); + updater.digest = Some(ConfigDigest::from([0u8; 32])); + } } -- 2.47.3