[pve-devel] [PATCH proxmox-ve-rs v2 01/15] sdn-types: initial commit
Gabriel Goller
g.goller at proxmox.com
Fri Apr 4 18:28:13 CEST 2025
From: Stefan Hanreich <s.hanreich at proxmox.com>
This crate contains SDN specific types, so they can be re-used across
multiple crates (The initial use-case being shared types between
proxmox-frr and proxmox-ve-config).
This initial commit contains types for the following entities:
* OpenFabric Hello Interval/Multiplier and CSNP Interval
* Network Entity Title (used as Router IDs in IS-IS / OpenFabric)
Signed-off-by: Stefan Hanreich <s.hanreich at proxmox.com>
Signed-off-by: Gabriel Goller <g.goller at proxmox.com>
---
Cargo.toml | 4 +
proxmox-sdn-types/Cargo.toml | 14 +
proxmox-sdn-types/debian/changelog | 5 +
proxmox-sdn-types/debian/control | 39 +++
proxmox-sdn-types/debian/copyright | 18 ++
proxmox-sdn-types/debian/debcargo.toml | 7 +
proxmox-sdn-types/src/lib.rs | 2 +
proxmox-sdn-types/src/net.rs | 382 +++++++++++++++++++++++++
proxmox-sdn-types/src/openfabric.rs | 89 ++++++
proxmox-ve-config/Cargo.toml | 6 +-
10 files changed, 563 insertions(+), 3 deletions(-)
create mode 100644 proxmox-sdn-types/Cargo.toml
create mode 100644 proxmox-sdn-types/debian/changelog
create mode 100644 proxmox-sdn-types/debian/control
create mode 100644 proxmox-sdn-types/debian/copyright
create mode 100644 proxmox-sdn-types/debian/debcargo.toml
create mode 100644 proxmox-sdn-types/src/lib.rs
create mode 100644 proxmox-sdn-types/src/net.rs
create mode 100644 proxmox-sdn-types/src/openfabric.rs
diff --git a/Cargo.toml b/Cargo.toml
index b6e6df77969b..bd6a9ca4a79a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,7 @@
[workspace]
members = [
"proxmox-ve-config",
+ "proxmox-sdn-types",
]
exclude = [
"build",
@@ -17,3 +18,6 @@ rust-version = "1.82"
[workspace.dependencies]
proxmox-network-types = { version = "0.1" }
+serde = { version = "1" }
+serde_with = "3"
+thiserror = "1.0.59"
diff --git a/proxmox-sdn-types/Cargo.toml b/proxmox-sdn-types/Cargo.toml
new file mode 100644
index 000000000000..b3dc98550a57
--- /dev/null
+++ b/proxmox-sdn-types/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "proxmox-sdn-types"
+version = "0.1.0"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+homepage.workspace = true
+exclude.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+thiserror = { workspace = true }
+serde = { workspace = true, features = [ "derive" ] }
+serde_with = { workspace = true }
diff --git a/proxmox-sdn-types/debian/changelog b/proxmox-sdn-types/debian/changelog
new file mode 100644
index 000000000000..422921c2d1f4
--- /dev/null
+++ b/proxmox-sdn-types/debian/changelog
@@ -0,0 +1,5 @@
+rust-proxmox-sdn-types (0.1.0-1) unstable; urgency=medium
+
+ * Initial release.
+
+ -- Proxmox Support Team <support at proxmox.com> Mon, 03 Jun 2024 10:51:11 +0200
diff --git a/proxmox-sdn-types/debian/control b/proxmox-sdn-types/debian/control
new file mode 100644
index 000000000000..16a25313ad37
--- /dev/null
+++ b/proxmox-sdn-types/debian/control
@@ -0,0 +1,39 @@
+Source: rust-proxmox-sdn-types
+Section: rust
+Priority: optional
+Build-Depends: debhelper-compat (= 13),
+ dh-sequence-cargo
+Build-Depends-Arch: cargo:native <!nocheck>,
+ rustc:native (>= 1.82) <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-serde-1+default-dev <!nocheck>,
+ librust-serde-1+derive-dev <!nocheck>,
+ librust-serde-with-3+default-dev <!nocheck>,
+ librust-thiserror-1+default-dev (>= 1.0.59-~~) <!nocheck>
+Maintainer: Proxmox Support Team <support at proxmox.com>
+Standards-Version: 4.7.0
+Vcs-Git: git://git.proxmox.com/git/proxmox-ve-rs.git
+Vcs-Browser: https://git.proxmox.com/?p=proxmox-ve-rs.git
+Homepage: https://proxmox.com
+X-Cargo-Crate: proxmox-sdn-types
+Rules-Requires-Root: no
+
+Package: librust-proxmox-sdn-types-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-serde-1+default-dev,
+ librust-serde-1+derive-dev,
+ librust-serde-with-3+default-dev,
+ librust-thiserror-1+default-dev (>= 1.0.59-~~)
+Provides:
+ librust-proxmox-sdn-types+default-dev (= ${binary:Version}),
+ librust-proxmox-sdn-types-0-dev (= ${binary:Version}),
+ librust-proxmox-sdn-types-0+default-dev (= ${binary:Version}),
+ librust-proxmox-sdn-types-0.1-dev (= ${binary:Version}),
+ librust-proxmox-sdn-types-0.1+default-dev (= ${binary:Version}),
+ librust-proxmox-sdn-types-0.1.0-dev (= ${binary:Version}),
+ librust-proxmox-sdn-types-0.1.0+default-dev (= ${binary:Version})
+Description: Rust crate "proxmox-sdn-types" - Rust source code
+ Source code for Debianized Rust crate "proxmox-sdn-types"
diff --git a/proxmox-sdn-types/debian/copyright b/proxmox-sdn-types/debian/copyright
new file mode 100644
index 000000000000..1ea8a56b4f58
--- /dev/null
+++ b/proxmox-sdn-types/debian/copyright
@@ -0,0 +1,18 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+
+Files:
+ *
+Copyright: 2019 - 2025 Proxmox Server Solutions GmbH <support at proxmox.com>
+License: AGPL-3.0-or-later
+ This program is free software: you can redistribute it and/or modify it under
+ the terms of the GNU Affero General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option) any
+ later version.
+ .
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ details.
+ .
+ You should have received a copy of the GNU Affero General Public License along
+ with this program. If not, see <https://www.gnu.org/licenses/>.
diff --git a/proxmox-sdn-types/debian/debcargo.toml b/proxmox-sdn-types/debian/debcargo.toml
new file mode 100644
index 000000000000..87a787e6d03e
--- /dev/null
+++ b/proxmox-sdn-types/debian/debcargo.toml
@@ -0,0 +1,7 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support at proxmox.com>"
+
+[source]
+vcs_git = "git://git.proxmox.com/git/proxmox-ve-rs.git"
+vcs_browser = "https://git.proxmox.com/?p=proxmox-ve-rs.git"
diff --git a/proxmox-sdn-types/src/lib.rs b/proxmox-sdn-types/src/lib.rs
new file mode 100644
index 000000000000..018674612710
--- /dev/null
+++ b/proxmox-sdn-types/src/lib.rs
@@ -0,0 +1,2 @@
+pub mod net;
+pub mod openfabric;
diff --git a/proxmox-sdn-types/src/net.rs b/proxmox-sdn-types/src/net.rs
new file mode 100644
index 000000000000..97e019383bcc
--- /dev/null
+++ b/proxmox-sdn-types/src/net.rs
@@ -0,0 +1,382 @@
+use std::{
+ fmt::Display,
+ net::{IpAddr, Ipv4Addr, Ipv6Addr},
+ str::FromStr,
+};
+
+use serde::Serialize;
+use serde_with::{DeserializeFromStr, SerializeDisplay};
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum NetError {
+ #[error("Some octets are missing")]
+ WrongLength,
+ #[error("The NET selector must be two characters wide and be 00")]
+ InvalidNetSelector,
+ #[error("Invalid AFI (wrong size or position)")]
+ InvalidAFI,
+ #[error("Invalid Area (wrong size or position)")]
+ InvalidArea,
+ #[error("Invalid SystemId (wrong size or position)")]
+ InvalidSystemId,
+}
+
+/// Address Family authority Identifier - 49 The AFI value 49 is what IS-IS (and openfabric) uses
+/// for private addressing.
+#[derive(
+ Debug, DeserializeFromStr, SerializeDisplay, Clone, Hash, PartialEq, Eq, PartialOrd, Ord,
+)]
+struct NetAFI(String);
+
+impl Default for NetAFI {
+ fn default() -> Self {
+ Self("49".to_owned())
+ }
+}
+
+impl Display for NetAFI {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.0)
+ }
+}
+
+impl FromStr for NetAFI {
+ type Err = NetError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ if s.len() != 2 {
+ Err(NetError::InvalidAFI)
+ } else {
+ Ok(Self(s.to_owned()))
+ }
+ }
+}
+
+/// 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.
+#[derive(Debug, DeserializeFromStr, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
+struct NetArea(String);
+
+impl Default for NetArea {
+ fn default() -> Self {
+ Self("0001".to_owned())
+ }
+}
+
+impl Display for NetArea {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.0)
+ }
+}
+
+impl FromStr for NetArea {
+ type Err = NetError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ if s.len() != 4 {
+ Err(NetError::InvalidArea)
+ } else {
+ Ok(Self(s.to_owned()))
+ }
+ }
+}
+
+/// System identifier: 1921.6800.1002 - for system identifiers we recommend to use IP address or
+/// MAC address of the router itself. The way to construct this is to keep all of the zeroes of the
+/// router IP address, and then change the periods from being every three numbers to every four
+/// numbers. The address that is listed here is 192.168.1.2, which if expanded will turn into
+/// 192.168.001.002. Then all one has to do is move the dots to have four numbers instead of three.
+/// This gives us 1921.6800.1002.
+#[derive(Debug, DeserializeFromStr, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
+struct NetSystemId(String);
+
+impl Display for NetSystemId {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.0)
+ }
+}
+
+impl FromStr for NetSystemId {
+ type Err = NetError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ if s.split(".").count() != 3
+ || s.split(".").any(|segment| {
+ segment.len() != 4 || !segment.chars().all(|c| c.is_ascii_hexdigit())
+ })
+ {
+ Err(NetError::InvalidSystemId)
+ } else {
+ Ok(Self(s.to_owned()))
+ }
+ }
+}
+
+/// Convert IP-Address to a NET address with the default afi, area and selector values. Note that a
+/// valid Ipv4Addr is always a valid SystemId as well.
+impl From<Ipv4Addr> for NetSystemId {
+ fn from(value: Ipv4Addr) -> Self {
+ let octets = value.octets();
+
+ let system_id_str = format!(
+ "{:03}{:01}.{:02}{:02}.{:01}{:03}",
+ octets[0],
+ octets[1] / 100,
+ octets[1] % 100,
+ octets[2] / 10,
+ octets[2] % 10,
+ octets[3]
+ );
+
+ Self(system_id_str)
+ }
+}
+
+/// Convert IPv6-Address to a NET address with the default afi, area and selector values. Note that a
+/// valid Ipv6Addr is always a valid SystemId as well.
+impl From<Ipv6Addr> for NetSystemId {
+ fn from(value: Ipv6Addr) -> Self {
+ let segments = value.segments();
+ //
+ // Use the last 3 segments (out of 8) of the IPv6 address
+ let system_id_str = format!(
+ "{:04x}.{:04x}.{:04x}",
+ segments[5], segments[6], segments[7]
+ );
+
+ Self(system_id_str)
+ }
+}
+
+/// NET selector: 00 Must always be 00. This setting indicates “this system” or “local system.”
+#[derive(Debug, DeserializeFromStr, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
+struct NetSelector(String);
+
+impl Default for NetSelector {
+ fn default() -> Self {
+ Self("00".to_owned())
+ }
+}
+
+impl Display for NetSelector {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.0)
+ }
+}
+
+impl FromStr for NetSelector {
+ type Err = NetError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ if s.len() != 2 {
+ Err(NetError::InvalidNetSelector)
+ } else {
+ Ok(Self(s.to_owned()))
+ }
+ }
+}
+
+/// The OpenFabric Net.
+///
+/// Every OpenFabric node and fabric is identified through the NET. It has a network and a host
+/// part.
+/// The first part is the network part (also called area). The entire OpenFabric fabric has to have
+/// the same network part (area). The first number is the [`NetAFI`] and the second is the [`NetArea`].
+/// e.g.: "49.0001"
+/// The second part is the host part, which has to differ on every node in the fabric. It contains
+/// the [`NetSystemId`] and the [`NetSelector`].
+/// e.g.: "1921.6800.1002.00"
+#[derive(
+ Debug, DeserializeFromStr, SerializeDisplay, Clone, Hash, PartialEq, Eq, PartialOrd, Ord,
+)]
+pub struct Net {
+ afi: NetAFI,
+ area: NetArea,
+ system: NetSystemId,
+ selector: NetSelector,
+}
+
+impl FromStr for Net {
+ type Err = NetError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ if s.split(".").count() != 6 {
+ return Err(NetError::WrongLength);
+ }
+ let mut iter = s.split(".");
+ let afi = iter.next().ok_or(NetError::WrongLength)?;
+ let area = iter.next().ok_or(NetError::WrongLength)?;
+ let system = format!(
+ "{}.{}.{}",
+ iter.next().ok_or(NetError::WrongLength)?,
+ iter.next().ok_or(NetError::WrongLength)?,
+ iter.next().ok_or(NetError::WrongLength)?
+ );
+ let selector = iter.next().ok_or(NetError::WrongLength)?;
+ Ok(Self {
+ afi: afi.parse()?,
+ area: area.parse()?,
+ system: system.parse()?,
+ selector: selector.parse()?,
+ })
+ }
+}
+
+/// Default NET address for a given Ipv4Addr. This adds the default afi, area and selector to the
+/// address.
+impl From<Ipv4Addr> for Net {
+ fn from(value: Ipv4Addr) -> Self {
+ Self {
+ afi: NetAFI::default(),
+ area: NetArea::default(),
+ system: value.into(),
+ selector: NetSelector::default(),
+ }
+ }
+}
+
+/// Default NET address for a given Ipv6Addr. This adds the default afi, area and selector to the
+/// address.
+impl From<Ipv6Addr> for Net {
+ fn from(value: Ipv6Addr) -> Self {
+ Self {
+ afi: NetAFI::default(),
+ area: NetArea::default(),
+ system: value.into(),
+ selector: NetSelector::default(),
+ }
+ }
+}
+
+/// Default NET address for a given IpAddr (can be either Ipv4 or Ipv6). This adds the default afi,
+/// area and selector to the address.
+impl From<IpAddr> for Net {
+ fn from(value: IpAddr) -> Self {
+ Self {
+ afi: NetAFI::default(),
+ area: NetArea::default(),
+ system: match value {
+ IpAddr::V4(ipv4_addr) => ipv4_addr.into(),
+ IpAddr::V6(ipv6_addr) => ipv6_addr.into(),
+ },
+ selector: NetSelector::default(),
+ }
+ }
+}
+
+impl Display for Net {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "{}.{}.{}.{}",
+ self.afi, self.area, self.system, self.selector
+ )
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_net_from_str() {
+ let input = "49.0001.1921.6800.1002.00";
+ let net = input.parse::<Net>().expect("this net should parse");
+ assert_eq!(net.afi, NetAFI("49".to_owned()));
+ assert_eq!(net.area, NetArea("0001".to_owned()));
+ assert_eq!(net.system, NetSystemId("1921.6800.1002".to_owned()));
+ assert_eq!(net.selector, NetSelector("00".to_owned()));
+
+ let input = "45.0200.0100.1001.ba1f.01";
+ let net = input.parse::<Net>().expect("this net should parse");
+ assert_eq!(net.afi, NetAFI("45".to_owned()));
+ assert_eq!(net.area, NetArea("0200".to_owned()));
+ assert_eq!(net.system, NetSystemId("0100.1001.ba1f".to_owned()));
+ assert_eq!(net.selector, NetSelector("01".to_owned()));
+ }
+
+ #[test]
+ fn test_net_from_str_failed() {
+ let input = "49.0001.1921.6800.1002.000";
+ assert!(matches!(
+ input.parse::<Net>(),
+ Err(NetError::InvalidNetSelector)
+ ));
+
+ let input = "49.0001.1921.6800.1002.00.00";
+ assert!(matches!(input.parse::<Net>(), Err(NetError::WrongLength)));
+
+ let input = "49.0001.1921.6800.10002.00";
+ assert!(matches!(
+ input.parse::<Net>(),
+ Err(NetError::InvalidSystemId)
+ ));
+
+ let input = "49.0001.1921.6800.1z02.00";
+ assert!(matches!(
+ input.parse::<Net>(),
+ Err(NetError::InvalidSystemId)
+ ));
+
+ let input = "409.0001.1921.6800.1002.00";
+ assert!(matches!(input.parse::<Net>(), Err(NetError::InvalidAFI)));
+
+ let input = "49.00001.1921.6800.1002.00";
+ assert!(matches!(input.parse::<Net>(), Err(NetError::InvalidArea)));
+ }
+
+ #[test]
+ fn test_net_display() {
+ let net = Net {
+ afi: NetAFI("49".to_owned()),
+ area: NetArea("0001".to_owned()),
+ system: NetSystemId("1921.6800.1002".to_owned()),
+ selector: NetSelector("00".to_owned()),
+ };
+ assert_eq!(format!("{net}"), "49.0001.1921.6800.1002.00");
+ }
+
+ #[test]
+ fn test_net_from_ipv4() {
+ let ip: Ipv4Addr = "192.168.1.100".parse().unwrap();
+ let net: Net = ip.into();
+ assert_eq!(format!("{net}"), "49.0001.1921.6800.1100.00");
+
+ let ip1: Ipv4Addr = "10.10.2.245".parse().unwrap();
+ let net1: Net = ip1.into();
+ assert_eq!(format!("{net1}"), "49.0001.0100.1000.2245.00");
+
+ let ip2: Ipv4Addr = "1.1.1.1".parse().unwrap();
+ let net2: Net = ip2.into();
+ assert_eq!(format!("{net2}"), "49.0001.0010.0100.1001.00");
+ }
+
+ #[test]
+ fn test_net_from_ipv6() {
+ // 2001:db8::1 -> [2001, 0db8, 0, 0, 0, 0, 0, 1]
+ // last 3 segments: [0, 0, 1]
+ let ip: Ipv6Addr = "2001:db8::1".parse().unwrap();
+ let net: Net = ip.into();
+ assert_eq!(format!("{net}"), "49.0001.0000.0000.0001.00");
+
+ // fe80::1234:5678:abcd -> [fe80, 0, 0, 0, 0, 1234, 5678, abcd]
+ // last 3 segments: [1234, 5678, abcd]
+ let ip1: Ipv6Addr = "fe80::1234:5678:abcd".parse().unwrap();
+ let net1: Net = ip1.into();
+ assert_eq!(format!("{net1}"), "49.0001.1234.5678.abcd.00");
+
+ // 2001:0db8:85a3::8a2e:370:7334 -> [2001, 0db8, 85a3, 0, 0, 8a2e, 0370, 7334]
+ // last 3 segments: [8a2e, 0370, 7334]
+ let ip2: Ipv6Addr = "2001:0db8:85a3::8a2e:370:7334".parse().unwrap();
+ let net2: Net = ip2.into();
+ assert_eq!(format!("{net2}"), "49.0001.8a2e.0370.7334.00");
+
+ // ::1 -> [0, 0, 0, 0, 0, 0, 0, 1]
+ // last 3 segments: [0, 0, 1]
+ let ip3: Ipv6Addr = "::1".parse().unwrap();
+ let net3: Net = ip3.into();
+ assert_eq!(format!("{net3}"), "49.0001.0000.0000.0001.00");
+ }
+}
diff --git a/proxmox-sdn-types/src/openfabric.rs b/proxmox-sdn-types/src/openfabric.rs
new file mode 100644
index 000000000000..f3fce5dcca7c
--- /dev/null
+++ b/proxmox-sdn-types/src/openfabric.rs
@@ -0,0 +1,89 @@
+use std::{fmt::Display, num::ParseIntError};
+
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum IntegerRangeError {
+ #[error("The value must be between {min} and {max} seconds")]
+ OutOfRange { min: i32, max: i32 },
+ #[error("Error parsing to number")]
+ ParsingError(#[from] ParseIntError),
+}
+
+/// The OpenFabric CSNP Interval.
+///
+/// The Complete Sequence Number Packets (CSNP) interval in seconds. The interval range is 1 to
+/// 600.
+#[derive(Serialize, Deserialize, Hash, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+#[serde(try_from = "u16")]
+pub struct CsnpInterval(u16);
+
+impl TryFrom<u16> for CsnpInterval {
+ type Error = IntegerRangeError;
+
+ fn try_from(number: u16) -> Result<Self, Self::Error> {
+ if (1..=600).contains(&number) {
+ Ok(CsnpInterval(number))
+ } else {
+ Err(IntegerRangeError::OutOfRange { min: 1, max: 600 })
+ }
+ }
+}
+
+impl Display for CsnpInterval {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+/// The OpenFabric Hello Interval.
+///
+/// The Hello Interval for a given interface in seconds. The range is 1 to 600. Hello packets are
+/// used to establish and maintain adjacency between OpenFabric neighbors.
+#[derive(Serialize, Deserialize, Hash, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+#[serde(try_from = "u16")]
+pub struct HelloInterval(u16);
+
+impl TryFrom<u16> for HelloInterval {
+ type Error = IntegerRangeError;
+
+ fn try_from(number: u16) -> Result<Self, Self::Error> {
+ if (1..=600).contains(&number) {
+ Ok(HelloInterval(number))
+ } else {
+ Err(IntegerRangeError::OutOfRange { min: 1, max: 600 })
+ }
+ }
+}
+
+impl Display for HelloInterval {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+/// The OpenFabric Hello Multiplier.
+///
+/// This is the multiplier for the hello holding time on a given interface. The range is 2 to 100.
+#[derive(Serialize, Deserialize, Hash, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+#[serde(try_from = "u16")]
+pub struct HelloMultiplier(u16);
+
+impl TryFrom<u16> for HelloMultiplier {
+ type Error = IntegerRangeError;
+
+ fn try_from(number: u16) -> Result<Self, Self::Error> {
+ if (2..=100).contains(&number) {
+ Ok(HelloMultiplier(number))
+ } else {
+ Err(IntegerRangeError::OutOfRange { min: 2, max: 100 })
+ }
+ }
+}
+
+impl Display for HelloMultiplier {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml
index 2f3544bd5611..d8735e33653b 100644
--- a/proxmox-ve-config/Cargo.toml
+++ b/proxmox-ve-config/Cargo.toml
@@ -10,12 +10,12 @@ exclude.workspace = true
log = "0.4"
anyhow = "1"
nix = "0.26"
-thiserror = "1.0.59"
+thiserror = { workspace = true }
-serde = { version = "1", features = [ "derive" ] }
+serde = { workspace = true, features = [ "derive" ] }
serde_json = "1"
serde_plain = "1"
-serde_with = "3"
+serde_with = { workspace = true }
proxmox-network-types = { workspace = true }
proxmox-schema = "4"
--
2.39.5
More information about the pve-devel
mailing list