[pve-devel] [PATCH proxmox-ve-rs 02/11] add proxmox-frr crate with frr types

Stefan Hanreich s.hanreich at proxmox.com
Mon Mar 3 17:29:25 CET 2025


This uses stuff from a later patch, doesn't it? Shouldn't the order of
patches 2 and 3 be flipped?

On 2/14/25 14:39, Gabriel Goller wrote:
> 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 at proxmox.com>
> Signed-off-by: Gabriel Goller <g.goller at 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;

Maybe move it to network-types, if it is always needed? Seems like a
better fit. Especially since the dependency is optional.

> +#[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));
> +        }

does this make sense here? can we actually make a clear distinction on
whether this is a OpenFabric / OSPF router name (and for all other
future RouterNames) from the string alone? I think it might be better to
explicitly construct the specific type and then make a RouterName out of
it and do not implement FromStr at all. Or get rid of RouterName
altogether (see below).

It's also constructing an OpenFabric RouterName but never an OSPF router
name.

> +
> +        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),
> +}

maybe this should be a struct representing a linux interface name
(nul-terminated 16byte string) instead of an 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>,

are we ever querying the frr router/interface by name? judging from the
public API we don't and only iterate over it. we could move the name
into the Router/Interface then, this would ensure that the name always
fits the concrete router. We could probably also make converting from
the section config easier then and possibly save us the whole RouterName
struct.

Are duplicates possible with how the ID in the SectionConfig works? If
we want to avoid that, we could probably use other ways.

> +}
> +
> +impl FrrConfig {
> +    pub fn new() -> Self {
> +        Self::default()
> +    }
> +
> +    #[cfg(feature = "config-ext")]
> +    pub fn builder() -> FrrConfigBuilder {
> +        FrrConfigBuilder::default()
> +    }

see above for if we really need a builder here or if implementing
conversion traits suffices.

> +
> +    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
> +    }

From<FabricConfig> might be better if it replaces self.fabrics /
consumes FabricConfig anyway? Maybe even TryFrom<FabricConfig> for
FrrConfig itself?

> +
> +    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()?;

The only fallible thing here is constructing the RouterName, so we could
just clone it from above and make a
OpenFabricInterface::from_section_config() method that accepts the name
+ sectionconfig structs?

> +                            // 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");
> +                            }

if let Err(_) = interfaces.try_insert(..) maybe (if we keep the HashMap)?

> +                            Ok::<(), anyhow::Error>(())
> +                        })?;
> +                    } else {
> +                        tracing::warn!("no node configuration for fabric \"{fabric_id}\" – this fabric is not configured for this node.");

Maybe it would make sense to split this into two functions, where we
could just return early if there is no configuration for this node?

Then here an early return would suffice, since otherwise the log gets
spammed on nodes that are simply not part of a fabric (which is
perfectly valid)?

> +                        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 })

same points as above basically

> +    }
> +}
> 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
> +    }

If we implement Copy for those types it's usually just easier to return
them owned.

> +    pub fn set_hello_interval(&mut self, interval: Option<openfabric::HelloInterval>) {

nit: I usually like impl Into<Option<..>> because it makes the API nicer
(don't have to write Some(..) all the time ...)

> +        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(),

We can easily implement Copy for those values, since they're u16.

> +        })
> +    }
> +}
> +
> +#[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(),
> +        }
> +    }
> +}

We never use value.0 here, but we might if we have some global options,
right?

> 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")

this is missing the area, or?

> +    }
> +}
> +
> +#[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() {

use u32 here? otherwise area ID can be negative, which isn't allowed afaict?

> +            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,
> +        }
> +    }
> +}





More information about the pve-devel mailing list