public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Gabriel Goller <g.goller@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH proxmox-ve-rs 4/9] sdn-types: support variable-length NET identifier
Date: Tue,  3 Feb 2026 17:01:11 +0100	[thread overview]
Message-ID: <20260203160246.353351-5-g.goller@proxmox.com> (raw)
In-Reply-To: <20260203160246.353351-1-g.goller@proxmox.com>

The NET (Network Entity Title) can actually be variable-length. We only
use the minimum length one (which corresponds to an ipv4 address) in the
fabrics, but in the ISIS tests we also use a longer NET. Support the
longer NET as well.

This is because in the perl-frr-generation we support variable-length
NETs (this is also covered in the tests).

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 proxmox-sdn-types/src/net.rs | 136 +++++++++++++++++++++++++++++++----
 1 file changed, 121 insertions(+), 15 deletions(-)

diff --git a/proxmox-sdn-types/src/net.rs b/proxmox-sdn-types/src/net.rs
index 3cd1e4f80ed7..3e523fb12d9b 100644
--- a/proxmox-sdn-types/src/net.rs
+++ b/proxmox-sdn-types/src/net.rs
@@ -10,7 +10,8 @@ use proxmox_schema::{api, api_string_type, const_regex, ApiStringFormat, Updater
 
 const_regex! {
     NET_AFI_REGEX = r"^(?:[a-fA-F0-9]{2})$";
-    NET_AREA_REGEX = r"^(?:[a-fA-F0-9]{4})$";
+    // Variable length area: 0 to 13 bytes (0 to 26 hex digits) according to ISO 10589
+    NET_AREA_REGEX = r"^(?:[a-fA-F0-9]{0,26})$";
     NET_SYSTEM_ID_REGEX = r"^(?:[a-fA-F0-9]{4})\.(?:[a-fA-F0-9]{4})\.(?:[a-fA-F0-9]{4})$";
     NET_SELECTOR_REGEX = r"^(?:[a-fA-F0-9]{2})$";
 }
@@ -39,9 +40,9 @@ impl UpdaterType for NetAFI {
 }
 
 api_string_type! {
-    /// Area identifier: 0001 IS-IS area number (numerical area 1)
-    /// The second part (system) of the `net` identifier. Every node has to have a different system
-    /// number.
+    /// Area identifier: Variable length (0-13 bytes / 0-26 hex digits) according to ISO 10589
+    /// IS-IS area number that identifies the routing domain. All routers in the same area must
+    /// have the same area identifier. Can be empty or up to 26 hex digits.
     #[api(format: &NET_AREA_FORMAT)]
     #[derive(Debug, Deserialize, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
     struct NetArea(String);
@@ -146,6 +147,7 @@ pub struct Net {
     selector: NetSelector,
 }
 
+
 impl UpdaterType for Net {
     type Updater = Option<Net>;
 }
@@ -156,27 +158,58 @@ impl std::str::FromStr for Net {
     fn from_str(s: &str) -> Result<Self, Self::Err> {
         let parts: Vec<&str> = s.split(".").collect();
 
-        if parts.len() != 6 {
-            bail!("invalid NET format: {s}")
+        // Minimum: AFI.SystemID(3 parts).Selector = 5 parts
+        // With area: AFI.Area.SystemID(3 parts).Selector = 6+ parts
+        if parts.len() < 5 {
+            bail!("invalid NET format: {s} (expected at least AFI.SystemID.Selector)")
         }
 
-        let system = format!("{}.{}.{}", parts[2], parts[3], parts[4],);
+        // Last part is selector (2 hex digits)
+        let selector_idx = parts.len() - 1;
+        let selector = parts[selector_idx];
+
+        // Three parts before selector are system ID (xxxx.xxxx.xxxx)
+        let system_id_parts = &parts[selector_idx - 3..selector_idx];
+        let system = format!(
+            "{}.{}.{}",
+            system_id_parts[0], system_id_parts[1], system_id_parts[2]
+        );
+
+        // First part is AFI (2 hex digits)
+        let afi = parts[0];
+
+        // Everything between AFI and system ID is the area (can be empty)
+        let area_parts = &parts[1..selector_idx - 3];
+        let area = area_parts.join("");
 
         Ok(Self {
-            afi: NetAFI::from_string(parts[0].to_string())?,
-            area: NetArea::from_string(parts[1].to_string())?,
-            system: NetSystemId::from_string(system.to_string())?,
-            selector: NetSelector::from_string(parts[5].to_string())?,
+            afi: NetAFI::from_string(afi.to_string())?,
+            area: NetArea::from_string(area)?,
+            system: NetSystemId::from_string(system)?,
+            selector: NetSelector::from_string(selector.to_string())?,
         })
     }
 }
 
 impl Display for Net {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        // Format area with dots every 4 hex digits for readability
+        let area_str = self.area.0.as_str();
+        let area_formatted = if area_str.is_empty() {
+            String::new()
+        } else {
+            let chunks: Vec<&str> = area_str
+                .as_bytes()
+                .chunks(4)
+                .map(|chunk| std::str::from_utf8(chunk).unwrap())
+                .collect();
+            format!(".{}", chunks.join("."))
+        };
+
         write!(
             f,
-            "{}.{}.{}.{}",
-            self.afi, self.area, self.system, self.selector
+            "{}{}.{}.{}",
+            self.afi, area_formatted, self.system, self.selector
         )
     }
 }
@@ -258,8 +291,16 @@ mod tests {
         let input = "409.0001.1921.6800.1002.00";
         input.parse::<Net>().expect_err("invalid AFI");
 
-        let input = "49.00001.1921.6800.1002.00";
-        input.parse::<Net>().expect_err("invalid area");
+        // Area can now be variable length (0-26 hex digits), so 5 digits is valid
+        // but 27 digits would be invalid
+        let input = "49.0123.4567.8901.2345.6789.0123.4569.1921.6800.1002.00";
+        input
+            .parse::<Net>()
+            .expect_err("area too long (>26 hex digits)");
+
+        // Too few parts
+        let input = "49.1921.6800.00";
+        input.parse::<Net>().expect_err("not enough parts");
     }
 
     #[test]
@@ -320,4 +361,69 @@ mod tests {
         let net4: Net = ip4.into();
         assert_eq!(format!("{net4}"), "49.0001.0000.0000.0000.00");
     }
+
+    #[test]
+    fn test_net_variable_length_area() {
+        // Test with no area (just AFI)
+        let input = "49.1921.6800.1002.00";
+        let net = input.parse::<Net>().expect("should parse NET with no area");
+        assert_eq!(net.afi, NetAFI("49".to_owned()));
+        assert_eq!(net.area, NetArea("".to_owned()));
+        assert_eq!(net.system, NetSystemId("1921.6800.1002".to_owned()));
+        assert_eq!(net.selector, NetSelector("00".to_owned()));
+        assert_eq!(format!("{net}"), "49.1921.6800.1002.00");
+
+        // Test with 2 hex digit area
+        let input = "49.01.1921.6800.1002.00";
+        let net = input
+            .parse::<Net>()
+            .expect("should parse NET with 2-digit area");
+        assert_eq!(net.area, NetArea("01".to_owned()));
+        assert_eq!(format!("{net}"), "49.01.1921.6800.1002.00");
+
+        // Test with 4 hex digit area (standard)
+        let input = "49.0001.1921.6800.1002.00";
+        let net = input
+            .parse::<Net>()
+            .expect("should parse NET with 4-digit area");
+        assert_eq!(net.area, NetArea("0001".to_owned()));
+        assert_eq!(format!("{net}"), "49.0001.1921.6800.1002.00");
+
+        // Test with 8 hex digit area (formatted with dots every 4 digits)
+        let input = "49.0001.0002.1921.6800.1002.00";
+        let net = input
+            .parse::<Net>()
+            .expect("should parse NET with 8-digit area");
+        assert_eq!(net.area, NetArea("00010002".to_owned()));
+        // Should be formatted with dots every 4 hex digits
+        assert_eq!(format!("{net}"), "49.0001.0002.1921.6800.1002.00");
+
+        // Test with 12 hex digit area
+        let input = "49.0001.0002.0003.1921.6800.1002.00";
+        let net = input
+            .parse::<Net>()
+            .expect("should parse NET with 12-digit area");
+        assert_eq!(net.area, NetArea("000100020003".to_owned()));
+        assert_eq!(format!("{net}"), "49.0001.0002.0003.1921.6800.1002.00");
+
+        // Test with odd-length area (5 hex digits)
+        let input = "49.12345.1921.6800.1002.00";
+        let net = input
+            .parse::<Net>()
+            .expect("should parse NET with 5-digit area");
+        assert_eq!(net.area, NetArea("12345".to_owned()));
+        // Should be formatted with dots every 4 digits, last chunk has 1 digit
+        assert_eq!(format!("{net}"), "49.1234.5.1921.6800.1002.00");
+
+        // Test with maximum length area (26 hex digits = 13 bytes)
+        let input = "49.0123.4567.89ab.cdef.0123.4567.1921.6800.1002.00";
+        let net = input
+            .parse::<Net>()
+            .expect("should parse NET with 26-digit area");
+        assert_eq!(net.area, NetArea("0123456789abcdef01234567".to_owned()));
+        assert_eq!(
+            format!("{net}"),
+            "49.0123.4567.89ab.cdef.0123.4567.1921.6800.1002.00"
+        );
+    }
 }
-- 
2.47.3





  parent reply	other threads:[~2026-02-03 16:03 UTC|newest]

Thread overview: 24+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-02-03 16:01 [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} 00/23] Generate frr config using jinja templates and rust types Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 1/9] ve-config: firewall: cargo fmt Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 2/9] frr: add proxmox-frr-templates package that contains templates Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 3/9] ve-config: remove FrrConfigBuilder struct Gabriel Goller
2026-02-03 16:01 ` Gabriel Goller [this message]
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 5/9] frr: add template serializer and serialize fabrics using templates Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 6/9] frr: add isis configuration and templates Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 7/9] frr: support custom frr configuration lines Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 8/9] frr: add bgp support with templates and serialization Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-ve-rs 9/9] frr: store frr template content as a const map Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-perl-rs 1/2] sdn: add function to generate the frr config for all daemons Gabriel Goller
2026-02-03 16:01 ` [PATCH proxmox-perl-rs 2/2] sdn: add method to get a frr template Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 01/10] sdn: remove duplicate comment line '!' in frr config Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 02/10] sdn: tests: add missing comment " Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 03/10] tests: use Test::Differences to make test assertions Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 04/10] sdn: write structured frr config that can be rendered using templates Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 05/10] tests: rearrange some statements in the frr config Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 06/10] sdn: adjust frr.conf.local merging to rust template types Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 07/10] cli: add pvesdn cli tool for managing frr template overrides Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 08/10] debian: handle user modifications to FRR templates via ucf Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 09/10] api: add dry-run endpoint for sdn apply to preview changes Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-network 10/10] test: add test for frr.conf.local merging Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-manager 1/1] sdn: add dry-run view for sdn apply Gabriel Goller
2026-02-03 16:01 ` [PATCH pve-docs 1/1] docs: add man page for the `pvesdn` cli Gabriel Goller

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260203160246.353351-5-g.goller@proxmox.com \
    --to=g.goller@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal