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
next prev 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