all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Gabriel Goller <g.goller@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH proxmox-ve-rs 02/11] add proxmox-frr crate with frr types
Date: Fri, 14 Feb 2025 14:39:42 +0100	[thread overview]
Message-ID: <20250214133951.344500-3-g.goller@proxmox.com> (raw)
In-Reply-To: <20250214133951.344500-1-g.goller@proxmox.com>

This crate contains types that represent the frr config. For example it
contains a `Router` and `Interface` struct. This Frr-Representation can
then be converted to the real frr config.

Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 Cargo.toml                    |   1 +
 proxmox-frr/Cargo.toml        |  25 ++++
 proxmox-frr/src/common.rs     |  54 ++++++++
 proxmox-frr/src/lib.rs        | 223 ++++++++++++++++++++++++++++++++++
 proxmox-frr/src/openfabric.rs | 137 +++++++++++++++++++++
 proxmox-frr/src/ospf.rs       | 148 ++++++++++++++++++++++
 6 files changed, 588 insertions(+)
 create mode 100644 proxmox-frr/Cargo.toml
 create mode 100644 proxmox-frr/src/common.rs
 create mode 100644 proxmox-frr/src/lib.rs
 create mode 100644 proxmox-frr/src/openfabric.rs
 create mode 100644 proxmox-frr/src/ospf.rs

diff --git a/Cargo.toml b/Cargo.toml
index e452c931e78c..ffda1233b17a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,7 @@
 [workspace]
 members = [
     "proxmox-ve-config",
+    "proxmox-frr",
     "proxmox-network-types",
 ]
 exclude = [
diff --git a/proxmox-frr/Cargo.toml b/proxmox-frr/Cargo.toml
new file mode 100644
index 000000000000..bea8a0f8bab3
--- /dev/null
+++ b/proxmox-frr/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "proxmox-frr"
+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 }
+anyhow = "1"
+tracing = "0.1"
+
+serde = { workspace = true, features = [ "derive" ] }
+serde_with = { workspace = true }
+itoa = "1.0.9"
+
+proxmox-ve-config = { path = "../proxmox-ve-config", optional = true }
+proxmox-section-config = { workspace = true, optional = true }
+proxmox-network-types = { path = "../proxmox-network-types/" }
+
+[features]
+config-ext = ["dep:proxmox-ve-config", "dep:proxmox-section-config" ]
diff --git a/proxmox-frr/src/common.rs b/proxmox-frr/src/common.rs
new file mode 100644
index 000000000000..0d99bb4da6e2
--- /dev/null
+++ b/proxmox-frr/src/common.rs
@@ -0,0 +1,54 @@
+use std::{fmt::Display, str::FromStr};
+
+use serde_with::{DeserializeFromStr, SerializeDisplay};
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum FrrWordError {
+    #[error("word is empty")]
+    IsEmpty,
+    #[error("word contains invalid character")]
+    InvalidCharacter,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, DeserializeFromStr, SerializeDisplay)]
+pub struct FrrWord(String);
+
+impl FrrWord {
+    pub fn new(name: String) -> Result<Self, FrrWordError> {
+        if name.is_empty() {
+            return Err(FrrWordError::IsEmpty);
+        }
+
+        if name
+            .as_bytes()
+            .iter()
+            .any(|c| !c.is_ascii() || c.is_ascii_whitespace())
+        {
+            return Err(FrrWordError::InvalidCharacter);
+        }
+
+        Ok(Self(name))
+    }
+}
+
+impl FromStr for FrrWord {
+    type Err = FrrWordError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        FrrWord::new(s.to_string())
+    }
+}
+
+impl Display for FrrWord {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+impl AsRef<str> for FrrWord {
+    fn as_ref(&self) -> &str {
+        &self.0
+    }
+}
+
diff --git a/proxmox-frr/src/lib.rs b/proxmox-frr/src/lib.rs
new file mode 100644
index 000000000000..ceef82999619
--- /dev/null
+++ b/proxmox-frr/src/lib.rs
@@ -0,0 +1,223 @@
+pub mod common;
+pub mod openfabric;
+pub mod ospf;
+
+use std::{collections::{hash_map::Entry, HashMap}, fmt::Display, str::FromStr};
+
+use common::{FrrWord, FrrWordError};
+use proxmox_ve_config::sdn::fabric::common::Hostname;
+#[cfg(feature = "config-ext")]
+use proxmox_ve_config::sdn::fabric::FabricConfig;
+
+use serde::{Deserialize, Serialize};
+use serde_with::{DeserializeFromStr, SerializeDisplay};
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum RouterNameError {
+    #[error("invalid name")]
+    InvalidName,
+    #[error("invalid frr word")]
+    FrrWordError(#[from] FrrWordError),
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
+pub enum Router {
+    OpenFabric(openfabric::OpenFabricRouter),
+    Ospf(ospf::OspfRouter),
+}
+
+impl From<openfabric::OpenFabricRouter> for Router {
+    fn from(value: openfabric::OpenFabricRouter) -> Self {
+        Router::OpenFabric(value)
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, DeserializeFromStr, SerializeDisplay)]
+pub enum RouterName {
+    OpenFabric(openfabric::OpenFabricRouterName),
+    Ospf(ospf::OspfRouterName),
+}
+
+impl From<openfabric::OpenFabricRouterName> for RouterName {
+    fn from(value: openfabric::OpenFabricRouterName) -> Self {
+        Self::OpenFabric(value)
+    }
+}
+
+impl Display for RouterName {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::OpenFabric(r) => r.fmt(f),
+            Self::Ospf(r) => r.fmt(f),
+        }
+    }
+}
+
+impl FromStr for RouterName {
+    type Err = RouterNameError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if let Ok(router) = s.parse() {
+            return Ok(Self::OpenFabric(router));
+        }
+
+        Err(RouterNameError::InvalidName)
+    }
+}
+
+/// The interface name is the same on ospf and openfabric, but it is an enum so we can have two
+/// different entries in the hashmap. This allows us to have an interface in an ospf and openfabric
+/// fabric.
+#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)]
+pub enum InterfaceName {
+    OpenFabric(FrrWord),
+    Ospf(FrrWord),
+}
+
+impl Display for InterfaceName {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            InterfaceName::OpenFabric(frr_word) => frr_word.fmt(f),
+            InterfaceName::Ospf(frr_word) => frr_word.fmt(f),
+        }
+        
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
+pub enum Interface {
+    OpenFabric(openfabric::OpenFabricInterface),
+    Ospf(ospf::OspfInterface),
+}
+
+impl From<openfabric::OpenFabricInterface> for Interface {
+    fn from(value: openfabric::OpenFabricInterface) -> Self {
+        Self::OpenFabric(value)
+    }
+}
+
+impl From<ospf::OspfInterface> for Interface {
+    fn from(value: ospf::OspfInterface) -> Self {
+        Self::Ospf(value)
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Default, Deserialize, Serialize)]
+pub struct FrrConfig {
+    router: HashMap<RouterName, Router>,
+    interfaces: HashMap<InterfaceName, Interface>,
+}
+
+impl FrrConfig {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    #[cfg(feature = "config-ext")]
+    pub fn builder() -> FrrConfigBuilder {
+        FrrConfigBuilder::default()
+    }
+
+    pub fn router(&self) -> impl Iterator<Item = (&RouterName, &Router)> + '_ {
+        self.router.iter()
+    }
+
+    pub fn interfaces(&self) -> impl Iterator<Item = (&InterfaceName, &Interface)> + '_ {
+        self.interfaces.iter()
+    }
+}
+
+#[derive(Default)]
+#[cfg(feature = "config-ext")]
+pub struct FrrConfigBuilder {
+    fabrics: FabricConfig,
+    //bgp: Option<internal::BgpConfig>
+}
+
+#[cfg(feature = "config-ext")]
+impl FrrConfigBuilder {
+    pub fn add_fabrics(mut self, fabric: FabricConfig) -> FrrConfigBuilder {
+        self.fabrics = fabric;
+        self
+    }
+
+    pub fn build(self, current_node: &str) -> Result<FrrConfig, anyhow::Error> {
+        let mut router: HashMap<RouterName, Router> = HashMap::new();
+        let mut interfaces: HashMap<InterfaceName, Interface> = HashMap::new();
+
+        if let Some(openfabric) = self.fabrics.openfabric() {
+            // openfabric
+            openfabric
+                .fabrics()
+                .iter()
+                .try_for_each(|(fabric_id, fabric_config)| {
+                    let node_config = fabric_config.nodes().get(&Hostname::new(current_node));
+                    if let Some(node_config) = node_config {
+                        let ofr = openfabric::OpenFabricRouter::from((fabric_config, node_config));
+                        let router_item = Router::OpenFabric(ofr);
+                        let router_name = RouterName::OpenFabric(
+                            openfabric::OpenFabricRouterName::try_from(fabric_id)?,
+                        );
+                        router.insert(router_name.clone(), router_item);
+                        node_config.interfaces().try_for_each(|interface| {
+                            let mut openfabric_interface: openfabric::OpenFabricInterface =
+                                (fabric_id, interface).try_into()?;
+                            // If no specific hello_interval is set, get default one from fabric
+                            // config
+                            if openfabric_interface.hello_interval().is_none() {
+                                openfabric_interface
+                                    .set_hello_interval(fabric_config.hello_interval().clone());
+                            }
+                            let interface_name = InterfaceName::OpenFabric(FrrWord::from_str(interface.name())?);
+                            // Openfabric doesn't allow an interface to be in multiple openfabric
+                            // fabrics. Frr will just ignore it and take the first one.
+                            if let Entry::Vacant(e) = interfaces.entry(interface_name) {
+                                e.insert(openfabric_interface.into());
+                            } else {
+                                tracing::warn!("An interface cannot be in multiple openfabric fabrics");
+                            }
+                            Ok::<(), anyhow::Error>(())
+                        })?;
+                    } else {
+                        tracing::warn!("no node configuration for fabric \"{fabric_id}\" – this fabric is not configured for this node.");
+                        return Ok::<(), anyhow::Error>(());
+                    }
+                    Ok(())
+                })?;
+        }
+        if let Some(ospf) = self.fabrics.ospf() {
+            // ospf
+            ospf.fabrics()
+                .iter()
+                .try_for_each(|(fabric_id, fabric_config)| {
+                    let node_config = fabric_config.nodes().get(&Hostname::new(current_node));
+                    if let Some(node_config) = node_config {
+                        let ospf_router = ospf::OspfRouter::from((fabric_config, node_config));
+                        let router_item = Router::Ospf(ospf_router);
+                        let router_name = RouterName::Ospf(ospf::OspfRouterName::from(ospf::Area::try_from(fabric_id)?));
+                        router.insert(router_name.clone(), router_item);
+                        node_config.interfaces().try_for_each(|interface| {
+                            let ospf_interface: ospf::OspfInterface = (fabric_id, interface).try_into()?;
+
+                            let interface_name = InterfaceName::Ospf(FrrWord::from_str(interface.name())?);
+                            // Ospf only allows one area per interface, so one interface cannot be
+                            // in two areas (fabrics). Though even if this happens, it is not a big
+                            // problem as frr filters it out.
+                            if let Entry::Vacant(e) = interfaces.entry(interface_name) {
+                                e.insert(ospf_interface.into());
+                            } else {
+                                tracing::warn!("An interface cannot be in multiple ospf areas");
+                            }
+                            Ok::<(), anyhow::Error>(())
+                        })?;
+                    } else {
+                        tracing::warn!("no node configuration for fabric \"{fabric_id}\" – this fabric is not configured for this node.");
+                        return Ok::<(), anyhow::Error>(()); 
+                    }
+                    Ok(())
+                })?;
+        }
+        Ok(FrrConfig { router, interfaces })
+    }
+}
diff --git a/proxmox-frr/src/openfabric.rs b/proxmox-frr/src/openfabric.rs
new file mode 100644
index 000000000000..12cfc61236cb
--- /dev/null
+++ b/proxmox-frr/src/openfabric.rs
@@ -0,0 +1,137 @@
+use std::fmt::Debug;
+use std::{fmt::Display, str::FromStr};
+
+use proxmox_network_types::net::Net;
+use serde::{Deserialize, Serialize};
+use serde_with::{DeserializeFromStr, SerializeDisplay};
+
+#[cfg(feature = "config-ext")]
+use proxmox_ve_config::sdn::fabric::openfabric::{self, internal};
+use thiserror::Error;
+
+use crate::common::FrrWord;
+use crate::RouterNameError;
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, DeserializeFromStr, SerializeDisplay)]
+pub struct OpenFabricRouterName(FrrWord);
+
+impl From<FrrWord> for OpenFabricRouterName {
+    fn from(value: FrrWord) -> Self {
+        Self(value)
+    }
+}
+
+impl OpenFabricRouterName {
+    pub fn new(name: FrrWord) -> Self {
+        Self(name)
+    }
+}
+
+impl FromStr for OpenFabricRouterName {
+    type Err = RouterNameError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if let Some(name) = s.strip_prefix("openfabric ") {
+            return Ok(Self::new(
+                FrrWord::from_str(name).map_err(|_| RouterNameError::InvalidName)?,
+            ));
+        }
+
+        Err(RouterNameError::InvalidName)
+    }
+}
+
+impl Display for OpenFabricRouterName {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "openfabric {}", self.0)
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
+pub struct OpenFabricRouter {
+    net: Net,
+}
+
+impl OpenFabricRouter {
+    pub fn new(net: Net) -> Self {
+        Self {
+            net,
+        }
+    }
+
+    pub fn net(&self) -> &Net {
+        &self.net
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
+pub struct OpenFabricInterface {
+    // Note: an interface can only be a part of a single fabric (so no vec needed here)
+    fabric_id: OpenFabricRouterName,
+    passive: Option<bool>,
+    hello_interval: Option<openfabric::HelloInterval>,
+    csnp_interval: Option<openfabric::CsnpInterval>,
+    hello_multiplier: Option<openfabric::HelloMultiplier>,
+}
+
+impl OpenFabricInterface {
+    pub fn fabric_id(&self) -> &OpenFabricRouterName {
+        &self.fabric_id
+    }
+    pub fn passive(&self) -> &Option<bool> {
+        &self.passive
+    }
+    pub fn hello_interval(&self) -> &Option<openfabric::HelloInterval> {
+        &self.hello_interval
+    }
+    pub fn csnp_interval(&self) -> &Option<openfabric::CsnpInterval> {
+        &self.csnp_interval
+    }
+    pub fn hello_multiplier(&self) -> &Option<openfabric::HelloMultiplier> {
+        &self.hello_multiplier
+    }
+    pub fn set_hello_interval(&mut self, interval: Option<openfabric::HelloInterval>) {
+        self.hello_interval = interval;
+    }
+}
+
+#[derive(Error, Debug)]
+pub enum OpenFabricInterfaceError {
+    #[error("Unknown error converting to OpenFabricInterface")]
+    UnknownError,
+    #[error("Error converting router name")]
+    RouterNameError(#[from] RouterNameError),
+}
+
+#[cfg(feature = "config-ext")]
+impl TryFrom<(&internal::FabricId, &internal::Interface)> for OpenFabricInterface {
+    type Error = OpenFabricInterfaceError;
+
+    fn try_from(value: (&internal::FabricId, &internal::Interface)) -> Result<Self, Self::Error> {
+        Ok(Self {
+            fabric_id: OpenFabricRouterName::try_from(value.0)?,
+            passive: value.1.passive(),
+            hello_interval: value.1.hello_interval().clone(),
+            csnp_interval: value.1.csnp_interval().clone(),
+            hello_multiplier: value.1.hello_multiplier().clone(),
+        })
+    }
+}
+
+#[cfg(feature = "config-ext")]
+impl TryFrom<&internal::FabricId> for OpenFabricRouterName {
+    type Error = RouterNameError;
+
+    fn try_from(value: &internal::FabricId) -> Result<Self, Self::Error> {
+        Ok(OpenFabricRouterName::new(FrrWord::new(value.to_string())?))
+    }
+}
+
+#[cfg(feature = "config-ext")]
+impl From<(&internal::FabricConfig, &internal::NodeConfig)> for OpenFabricRouter {
+    fn from(value: (&internal::FabricConfig, &internal::NodeConfig)) -> Self {
+        Self {
+            net: value.1.net().to_owned(),
+        }
+    }
+}
diff --git a/proxmox-frr/src/ospf.rs b/proxmox-frr/src/ospf.rs
new file mode 100644
index 000000000000..a14ef2c55c27
--- /dev/null
+++ b/proxmox-frr/src/ospf.rs
@@ -0,0 +1,148 @@
+use std::fmt::Debug;
+use std::net::Ipv4Addr;
+use std::{fmt::Display, str::FromStr};
+
+use serde::{Deserialize, Serialize};
+
+#[cfg(feature = "config-ext")]
+use proxmox_ve_config::sdn::fabric::ospf::internal;
+use thiserror::Error;
+
+use crate::common::{FrrWord, FrrWordError};
+
+/// The name of the ospf frr router. There is only one ospf fabric possible in frr (ignoring
+/// multiple invocations of the ospfd daemon) and the separation is done with areas. Still,
+/// different areas have the same frr router, so the name of the router is just "ospf" in "router
+/// ospf". This type still contains the Area so that we can insert it in the Hashmap.
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
+pub struct OspfRouterName(Area);
+
+impl From<Area> for OspfRouterName {
+    fn from(value: Area) -> Self {
+        Self(value)
+    }
+}
+
+impl Display for OspfRouterName {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "ospf")
+    }
+}
+
+#[derive(Error, Debug)]
+pub enum AreaParsingError {
+    #[error("Invalid area idenitifier. Area must be a number or an ipv4 address.")]
+    InvalidArea,
+    #[error("Invalid area idenitifier. Missing 'area' prefix.")]
+    MissingPrefix,
+    #[error("Error parsing to FrrWord")]
+    FrrWordError(#[from] FrrWordError),
+}
+
+/// The OSPF Area. Most commonly, this is just a number, e.g. 5, but sometimes also a
+/// pseudo-ipaddress, e.g. 0.0.0.0
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
+pub struct Area(FrrWord);
+
+impl TryFrom<FrrWord> for Area {
+    type Error = AreaParsingError;
+
+    fn try_from(value: FrrWord) -> Result<Self, Self::Error> {
+        Area::new(value)
+    }
+}
+
+impl Area {
+    pub fn new(name: FrrWord) -> Result<Self, AreaParsingError> {
+        if name.as_ref().parse::<i32>().is_ok() || name.as_ref().parse::<Ipv4Addr>().is_ok() {
+            Ok(Self(name))
+        } else {
+            Err(AreaParsingError::InvalidArea)
+        }
+    }
+}
+
+impl FromStr for Area {
+    type Err = AreaParsingError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if let Some(name) = s.strip_prefix("area ") {
+            return Self::new(FrrWord::from_str(name).map_err(|_| AreaParsingError::InvalidArea)?);
+        }
+
+        Err(AreaParsingError::MissingPrefix)
+    }
+}
+
+impl Display for Area {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "area {}", self.0)
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
+pub struct OspfRouter {
+    router_id: Ipv4Addr,
+}
+
+impl OspfRouter {
+    pub fn new(router_id: Ipv4Addr) -> Self {
+        Self { router_id }
+    }
+
+    pub fn router_id(&self) -> &Ipv4Addr {
+        &self.router_id
+    }
+}
+
+#[derive(Error, Debug)]
+pub enum OspfInterfaceParsingError {
+    #[error("Error parsing area")]
+    AreaParsingError(#[from] AreaParsingError)
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
+pub struct OspfInterface {
+    // Note: an interface can only be a part of a single area(so no vec needed here)
+    area: Area,
+    passive: Option<bool>,
+}
+
+impl OspfInterface {
+    pub fn area(&self) -> &Area {
+        &self.area
+    }
+    pub fn passive(&self) -> &Option<bool> {
+        &self.passive
+    }
+}
+
+#[cfg(feature = "config-ext")]
+impl TryFrom<(&internal::Area, &internal::Interface)> for OspfInterface {
+    type Error = OspfInterfaceParsingError;
+
+    fn try_from(value: (&internal::Area, &internal::Interface)) -> Result<Self, Self::Error> {
+        Ok(Self {
+            area: Area::try_from(value.0)?,
+            passive: value.1.passive(),
+        })
+    }
+}
+
+#[cfg(feature = "config-ext")]
+impl TryFrom<&internal::Area> for Area {
+    type Error = AreaParsingError;
+
+    fn try_from(value: &internal::Area) -> Result<Self, Self::Error> {
+        Area::new(FrrWord::new(value.to_string())?)
+    }
+}
+
+#[cfg(feature = "config-ext")]
+impl From<(&internal::FabricConfig, &internal::NodeConfig)> for OspfRouter {
+    fn from(value: (&internal::FabricConfig, &internal::NodeConfig)) -> Self {
+        Self {
+            router_id: value.1.router_id,
+        }
+    }
+}
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

  parent reply	other threads:[~2025-02-14 13:41 UTC|newest]

Thread overview: 32+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-02-14 13:39 [pve-devel] [RFC cluster/manager/network/proxmox{-ve-rs, -perl-rs} 00/11] Add SDN Fabrics Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH proxmox-ve-rs 01/11] add crate with common network types Gabriel Goller
2025-03-03 15:08   ` Stefan Hanreich
2025-03-05  8:28     ` Gabriel Goller
2025-02-14 13:39 ` Gabriel Goller [this message]
2025-03-03 16:29   ` [pve-devel] [PATCH proxmox-ve-rs 02/11] add proxmox-frr crate with frr types Stefan Hanreich
2025-03-04 16:28     ` Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH proxmox-ve-rs 03/11] add intermediate fabric representation Gabriel Goller
2025-02-28 13:57   ` Thomas Lamprecht
2025-02-28 16:19     ` Gabriel Goller
2025-03-04 17:30     ` Gabriel Goller
2025-03-05  9:03       ` Wolfgang Bumiller
2025-03-04  8:45   ` Stefan Hanreich
2025-03-05  9:09     ` Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH proxmox-perl-rs 04/11] fabrics: add CRUD and generate fabrics methods Gabriel Goller
2025-03-04  9:28   ` Stefan Hanreich
2025-03-05 10:20     ` Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH pve-cluster 05/11] cluster: add sdn fabrics config files Gabriel Goller
2025-02-28 12:19   ` Thomas Lamprecht
2025-02-28 12:52     ` Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH pve-network 06/11] add config file and common read/write methods Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH pve-network 07/11] merge the frr config with the fabrics frr config on apply Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH pve-network 08/11] add api endpoints for fabrics Gabriel Goller
2025-03-04  9:51   ` Stefan Hanreich
2025-02-14 13:39 ` [pve-devel] [PATCH pve-manager 09/11] sdn: add Fabrics view Gabriel Goller
2025-03-04  9:57   ` Stefan Hanreich
2025-03-07 15:57     ` Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH pve-manager 10/11] sdn: add fabric edit/delete forms Gabriel Goller
2025-03-04 10:07   ` Stefan Hanreich
2025-03-07 16:04     ` Gabriel Goller
2025-02-14 13:39 ` [pve-devel] [PATCH pve-manager 11/11] network: return loopback interface on network endpoint Gabriel Goller
2025-03-03 16:58 ` [pve-devel] [RFC cluster/manager/network/proxmox{-ve-rs, -perl-rs} 00/11] Add SDN Fabrics Stefan Hanreich

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=20250214133951.344500-3-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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal