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 3E8561FF13A for ; Wed, 01 Apr 2026 16:41:42 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id A53A631B36; Wed, 1 Apr 2026 16:40:52 +0200 (CEST) From: Stefan Hanreich To: pve-devel@lists.proxmox.com Subject: [PATCH proxmox-ve-rs v2 05/34] sdn-types: add common route-map helper types Date: Wed, 1 Apr 2026 16:39:14 +0200 Message-ID: <20260401143957.386809-6-s.hanreich@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260401143957.386809-1-s.hanreich@proxmox.com> References: <20260401143957.386809-1-s.hanreich@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1775054348130 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.708 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 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: K6CMSGH6PXORGN5ANWCD2R5ZMPPWZKEH X-Message-ID-Hash: K6CMSGH6PXORGN5ANWCD2R5ZMPPWZKEH X-MailFrom: s.hanreich@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: The reason for including those types here is that they are used in proxmox-frr for generating the FRR configuration as well as proxmox-ve-config for utilizing them in the section config types. For some values in route maps FRR supports specifying either an absolute value or a value relative to the existing value. When modifying the metric of a route via a route map, '123' would set the value to 123, whereas '+/-123' would add/subtract 123 from the existing value. IntegerWithSign can be used to represent such a value in the section config. A custom deserializer is implemented, so primitive numerical values can be used in addition to strings that contain the respective signs. They deserialize into absolute values. Signed-off-by: Stefan Hanreich --- proxmox-sdn-types/src/bgp.rs | 62 ++++++++++++ proxmox-sdn-types/src/lib.rs | 179 +++++++++++++++++++++++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 proxmox-sdn-types/src/bgp.rs diff --git a/proxmox-sdn-types/src/bgp.rs b/proxmox-sdn-types/src/bgp.rs new file mode 100644 index 0000000..6df3022 --- /dev/null +++ b/proxmox-sdn-types/src/bgp.rs @@ -0,0 +1,62 @@ +use serde::{Deserialize, Serialize}; + +use crate::IntegerWithSign; + +/// Represents a BGP metric value, as used in FRR. +/// +/// A metric can either be a numeric value, or certain 'magic' values. For more information see the +/// respective enum variants. +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +pub enum SetMetricValue { + /// Set the metric to the round-trip-time. + #[serde(rename = "rtt")] + Rtt, + /// Add the round-trip-time to the metric. + #[serde(rename = "+rtt")] + AddRtt, + /// Subtract the round-trip-time from the metric. + #[serde(rename = "-rtt")] + SubtractRtt, + /// Use the IGP value when importing from another IGP. + #[serde(rename = "igp")] + Igp, + /// Use the accumulated IGP value when importing from another IGP. + #[serde(rename = "aigp")] + Aigp, + /// Set the metric to a fixed numeric value. + #[serde(untagged)] + Numeric(IntegerWithSign), +} + +impl> From for SetMetricValue { + fn from(value: T) -> Self { + Self::Numeric(value.into()) + } +} + +/// An EVPN route-type, as used in the FRR route maps. +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum EvpnRouteType { + Ead, + MacIp, + Multicast, + #[serde(rename = "es")] + EthernetSegment, + Prefix, +} + +/// An tag value, as used in the FRR route maps. +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum SetTagValue { + Untagged, + #[serde(untagged)] + Numeric(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u32")] u32), +} + +impl SetTagValue { + pub fn new(value: u32) -> Self { + SetTagValue::Numeric(value) + } +} diff --git a/proxmox-sdn-types/src/lib.rs b/proxmox-sdn-types/src/lib.rs index 1656f1d..9795857 100644 --- a/proxmox-sdn-types/src/lib.rs +++ b/proxmox-sdn-types/src/lib.rs @@ -1,3 +1,182 @@ pub mod area; +pub mod bgp; pub mod net; pub mod openfabric; + +use serde::de::{Error, Visitor}; +use serde::{Deserialize, Serialize}; + +use proxmox_schema::api; + +/// Enum for representing signedness of Integer in [`IntegerWithSign`]. +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +pub enum Sign { + #[serde(rename = "-")] + Negative, + #[serde(rename = "+")] + Positive, +} + +proxmox_serde::forward_display_to_serialize!(Sign); +proxmox_serde::forward_from_str_to_deserialize!(Sign); + +/// An Integer with an optional [`Sign`]. +/// +/// This is used for representing certain keys in the FRR route maps (e.g. metric). They can be set +/// to either a static value (no sign) or to a value relative to the existing value (with sign). +/// For instance, a value of 50 would set the metric to 50, but a value of +50 would add 50 to the +/// existing metric value. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct IntegerWithSign { + pub(crate) sign: Option, + pub(crate) n: u32, +} + +impl IntegerWithSign { + pub fn new(sign: Option, n: u32) -> Self { + Self { sign, n } + } +} + +impl From for IntegerWithSign { + fn from(n: u32) -> Self { + Self { sign: None, n } + } +} + +impl std::fmt::Display for IntegerWithSign { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(sign) = self.sign.as_ref() { + write!(f, "{}{}", sign, self.n) + } else { + self.n.fmt(f) + } + } +} + +impl std::str::FromStr for IntegerWithSign { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + if let Some(n) = s.strip_prefix("+") { + return Ok(Self { + sign: Some(Sign::Positive), + n: n.parse()?, + }); + } + + if let Some(n) = s.strip_prefix("-") { + return Ok(Self { + sign: Some(Sign::Negative), + n: n.parse()?, + }); + } + + Ok(Self { + sign: None, + n: s.parse()?, + }) + } +} + +impl<'de> Deserialize<'de> for IntegerWithSign { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct V; + + impl<'de> Visitor<'de> for V { + type Value = IntegerWithSign; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("An integer with an optional leading sign") + } + + fn visit_i128(self, value: i128) -> Result { + Ok(IntegerWithSign { + sign: None, + n: u32::try_from(value).map_err(E::custom)?, + }) + } + + fn visit_i64(self, value: i64) -> Result { + Ok(IntegerWithSign { + sign: None, + n: u32::try_from(value).map_err(E::custom)?, + }) + } + + fn visit_u64(self, value: u64) -> Result { + Ok(IntegerWithSign { + sign: None, + n: u32::try_from(value).map_err(E::custom)?, + }) + } + + fn visit_u128(self, value: u128) -> Result { + Ok(IntegerWithSign { + sign: None, + n: u32::try_from(value).map_err(E::custom)?, + }) + } + + fn visit_str(self, v: &str) -> Result { + v.parse().map_err(E::custom) + } + } + + deserializer.deserialize_any(V) + } +} + +proxmox_serde::forward_serialize_to_display!(IntegerWithSign); + +#[api( + type: Integer, + minimum: 1, + maximum: 16_777_215, +)] +#[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)] +#[repr(transparent)] +/// Represents a VXLAN VNI (24-bit unsigned integer). +pub struct Vni(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u32")] u32); + +impl Vni { + /// Returns the VNI as u32. + pub fn as_u32(&self) -> u32 { + self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + #[test] + fn test_parse_integer_with_sign() { + assert_eq!( + IntegerWithSign::from_str("+32").expect("valid IntegerWithSign"), + IntegerWithSign::new(Some(Sign::Positive), 32) + ); + + assert_eq!( + IntegerWithSign::from_str("-31322").expect("valid IntegerWithSign"), + IntegerWithSign::new(Some(Sign::Negative), 31322) + ); + + assert_eq!( + IntegerWithSign::from_str("32").expect("valid IntegerWithSign"), + IntegerWithSign::new(None, 32) + ); + } + + #[test] + fn test_display_integer_with_sign() { + for s in &["+32", "-1234", "43344"] { + let integer_with_sign: IntegerWithSign = s.parse().expect("is a valid IntegerWithSign"); + assert_eq!(&integer_with_sign.to_string(), s) + } + } +} -- 2.47.3