* [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics
@ 2025-05-22 16:16 Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox v3 1/4] network-types: initial commit Stefan Hanreich
` (74 more replies)
0 siblings, 75 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
Overview
========
This series allows the user to easily use dynamic routing protocols such as
OpenFabric and OSPF in their clusters. It also integrates existing features,
such as Ceph with the new SDN fabrics feature to enable users simple
configuration of e.g. full-mesh Ceph clusters via the Web UI.
This patch series adds the initial support for two routing protocols:
* OpenFabric
* OSPF
In the future we plan on moving the existing IS-IS and BGP controllers into the
fabric structure. Christoph Heiss is also currently working on adding a new
Wireguard fabric, which can be combined with any other fabric types. This
feature allows layering different fabrics on top of each other, so adding
encryption to an existing fabric is as simple as just putting a Wireguard fabric
on top, or using a Wireguard fabric as the basis.
Packages are available on sani: packages/sdn-fabrics-v3
Implementation
==============
Every fabric consists of zero or more nodes, which themselves consist of zero or
more interfaces. Fabrics and nodes are modeled as different section config types
(which means two section types for each protocol), interfaces are an array
contained in a node section.
For now, nodes in the fabric configuration always represent PVE nodes, but in
the future nodes could also represent external members of the fabric (e.g. in a
potential Wireguard fabric). An example use case for this would be securely
connecting PBS or PDM instances to the PVE cluster via Wireguard.
Most of the functionality is implemented in Rust and exposed to the existing SDN
module via perlmod. This includes configuration reading / writing, FRR config
generation from the section config and API CRUD methods. Some functionality,
like digest matching and permission checking is still handled on the perl side,
due to the lack of facilities in Rust for that.
Configuration Format
--------------------
The whole configuration is now contained in just one configuration file
`/etc/pve/sdn/fabrics.cfg`. This makes handling the fabrics configuration easier
in many different areas: locking, digest calculation, validation.
For every protocol there are two different section types (fabric and node). As
an example the two section types for OSPF are 'ospf_fabric' and 'ospf_node'.
The ID of a fabric is a simple name, at most 8 alphanumeric characters since we
use it for generating network interfaces names with a prefix. This is analogous
to existing SDN entities, e.g. VNet. A node can only be uniquely identified by
its id (which is equivalent to the hostname of the node), as well as the
fabric_id. This is because a node can be part of multiple fabrics.
An example how the configuration looks like for a full-mesh 3-node Openfabric
fabric called 'example':
openfabric_fabric: example
csnp_interval 3
hello_interval 3
ip6_prefix 2001:db8::/64
ip_prefix 192.0.2.0/24
openfabric_node: example_deadeye
interfaces name=eth1,ip=198.51.100.0/31
interfaces name=eth2
ip 192.0.2.1
ip6 2001:db8::1
openfabric_node: example_pathfinder
interfaces name=eth1,ip=198.51.100.2/31
interfaces name=eth2
ip 192.0.2.2
ip6 2001:db8::2
openfabric_node: example_raider
interfaces name=eth1,ip=198.51.100.4/31
interfaces name=eth2
ip 192.0.2.3
ip6 2001:db8::3
We parse the configuration file flat in rust, and then afterwards split them
into Fabric / Node structs and store them hierarchically (fabric -> node) in a
dedicated FabricConfig struct. This struct provides the CRUD methods for
manipulating the FabricConfig safely as well as serializing the FabricConfig
back into its section config format.
To prevent having to duplicate common properties for every protocol, we
introduced a generic (Fabric|Node)Section<T> struct that contains all common
properties. Protocol-specific properties can be defined by the generic type
parameter T. This saves us from duplicating a lot of code (which was an initial
problem with the intermediate configuration) and conversions can be simplified
by providing a generic implementation that every protocol uses.
This design also means that adding new protocols to the configuration is quite
straightforward: It is only required to add structs with the protocol-specific
properties in Rust and add them to the enums defining the Section Config. The
commit adding OSPF support shows how simple it is to add a new protocol.
Validation
----------
The hierarchical nature of the configuration and the relationship between nodes
inside the fabrics requires validation of sections relative to other sections.
For this matter we introduced a new Validatable trait as well as a struct that
wraps valid configuration in a Valid<T> struct. For more information on that see
the respective commit.
API & Permissions
-----------------
The whole API is contained in the /cluster/sdn/fabrics subfolder and contains
submodules for fabric / node.
A quick overview of the methods provided by the API:
GET /all - list fabrics & nodes
GET /fabric - list all fabrics
POST /fabric - create a fabric
GET /fabric/{fabric_id} - get a single fabric
PUT /fabric/{fabric_id} - update a fabric
DELETE /fabric/{fabric_id} - delete a fabric
GET /node - list all nodes (regardless of fabric)
GET /node/{fabric_id} - list all nodes belonging to fabric {fabric_id}
POST /node/{fabric_id} - create a node in fabric {fabric_id}
GET /node/{fabric_id}/{node_id} - get a single node
PUT /node/{fabric_id}/{node_id} - update a single node
DELETE /node/{fabric_id}/{node_id} - delete a single node
FRR Configuration
-----------------
For the FRR-specific functionality we introduced a new proxmox-frr crate that
models the different entities in the FRR configuration format (routers,
interfaces, route-maps, ...) and provides serializers for those structs. For
more information see the respective FRR commits. When applying the SDN
configuration, perl calls into perlmod to utilize the proxmox-frr crate for
generating the FRR configuration of the fabrics.
We also introduce a proxmox-sdn-types crate, where we extracted generic
fabric types (e.g., openfabric::HelloInterval), so we can reuse them across
multiple crates (proxmox-frr, proxmox-ve-config, ..).
UI
--
The UI allows users to easily create different types of fabrics. One can add
Nodes to the fabrics by selecting them from a dropdown which shows all the nodes
in the cluster. Additionally the user can then select the interfaces of the node
which should be added to the fabric. There are also protocol-specific options
such as "passive", "hello-interval" etc. available to select on the interface.
There are also options spanning whole fabrics: the "hello-interval" option on
openfabric for example, can be set on the fabric and will be applied to every
interface.
We are also working on the integration of status reporting into the sidebar,
which also includes an integration into pvestatd. The plan is to show the status
for each node, which routes are learned, neighbor status and possibly topology
from the POV of each node. Since this patch series is already quite huge and the
sidebar integration is still a work in progress it is not included here.
Integration with existing features
----------------------------------
We also provide a UI for the Ceph, Migration Network, VXLAN zone and EVPN
controller integrations. Users can configure fabrics for those components simply
by selecting them from a dropdown, providing a streamlined experience and nice
integration with existing features.
Refactoring
===========
This patch series required some rework of existing functionality, mostly how SDN
generates the FRR configuration and writes /etc/network/interfaces. Prior the
FRR configuration was generated exclusively from the controllers, but fabrics
need to write it as well. Same goes for the interfaces file, which got written
by the Zone plugin, but Fabrics need to write this file as well.
For this we moved the FRR and ifupdown config generation one level up to the SDN
module, which now calls into the respective child modules to generate the FRR /
ifupdown configuration.
Dependencies
============
This series relies on the FRR 10.2.2 backport series, since it fixes potential
issues with EVPN + Openfabric/OSPF:
https://lore.proxmox.com/all/20250418112114.2747673-1-s.hanreich@proxmox.com/
proxmox-frr depends on proxmox-network-types
proxmox-frr depends on proxmox-sdn-types
proxmox-ve-config depends on proxmox-frr
proxmox-ve-config depends on proxmox-network-types
proxmox-ve-config depends on proxmox-sdn-types
proxmox-ve-config depends on proxmox-serde
proxmox-ve-config depends on proxmox-api-macro
proxmox-firewall depends on proxmox-ve-config
proxmox-perl-rs depends on proxmox-ve-config
proxmox-perl-rs depends on proxmox-frr
proxmox-perl-rs depends on proxmox-network-types
pve-network depends on proxmox-perl-rs
pve-network depends on pve-cluster
pve-network depends on pve-access-control
pve-docs depends on pve-gui-tests
pve-manager depends on proxmox-widget-toolkit
pve-manager depends on pve-docs
pve-manager depends on pve-network
pve-manager depends on pve-access-control
pve-network commits 4-7 do not build independently, because it's one refactor
but split across multiple commits so it's easier to follow the steps during the
refactor. We could consider squashing those commits on applying, so each commit
still builds indepedently.
Shoutout to Gabriel for his great work on this patch series!
Changelog v3:
=============
* Improved dual-stack support considerably
* Completely reworked configuration format and the respective Rust
representation (see above for more details)
* Completely reworked API design (see above for more details)
* refactored UI code and split NodeEdit panels into protocol-specific components
* Adapted permission paths to represent API structure
* Integrated Fabrics with the VXLAN zone + EVPN controller
* Added UI integration for fabrics to the following components:
* Migration Network Settings
* Ceph Installation Wizard
* VXLAN Zone
* EVPN controller
* added some quality of life features to the UI (e.g. Create another Node)
* many smaller bug fixes
Changelog v2:
=============
* split proxmox-network-types (this is done in a separate series)
* move Cidr-types and hostname to proxmox-network-types in the proxmox repo
* rename the proxmox-ve-rs/proxmox-network-types crate to proxmox-sdn-types
and put all the openfabric/ospf common types There
* fix ospf route-map generation and loopback_prefixes
* fix integration tests and add some more
* add fabric_id to OSPF, which acts as a primary (but arbitrary) id. The area
also has to be unique, but is only a required property now.
* this makes permissions easier, as every protocol has a "fabric_id" property we can check
* the users can choose a arbitrary name for the fabric and are not limited just by numbers and ip-addresses
* improve documentation wording
* add screenshots to documentation
* implement permissions in pve-access-control and pve-network
* made CRUD order in API modules and Common module consistent
* improve pve-network API descriptions
* improve pve-network API return types
* add helpers for common options
* refactored duplicated API types into a single variable inside the API
modules
* rework FRR reload logic - it now reloads only when daemons file stayed the
same, otherwise it restarts
* add fabric_id and node_id properties to the node section in OpenFabric and
OSPF (this allows us to be more generic over both protocols, useful in e.g.
frontend and permissions)
* make frontend components generic over protocols
* drop similar-asserts and use insta instead for integration tests
* added missing htmlencodes to tooltips / warning messages / tree column outputs
* hide action icons when node / fabric gets deleted
* added directory index to the root fabric method
* add digest to update calls
* improved format for fabrics in running configuration
* improved logic of set_daemon_status
* check for existence of /etc/frr/daemons before trying to read it
* OSPF interfaces now must have an IP or be unnumbered
Open issues:
Directory index is still missing for the ospf/openfabric subfolders, since we don't have
a 'GET /' endpoint there - could be added in a followup?
Network interfaces that have an entry in the interfaces file with the manual
stanza, do not get their IPs deconfigured when deleting the interfaces from a
fabric. This issue is documented.
Changelog v1:
=============
proxmox-ve-rs
-------------
* remove intermediate-config, convert section-config directly to frr-types.
* add validation layer to validate the section-config
* simplify openfabric `net` to `router-id`
* add loopback prefixes to ensure that all router-ids are in a specific subnet
* generate router-map and access-lists to rewrite the source address of all
the routes received through openfabric and ospf
* add integration tests
* add option for ospf unnumbered
* only allow ipv4 on ospf
pve-network
-------------
* rework frr config generation
* rework etc/network/interfaces config generation
* revert "return loopback interface"
proxmox-perl-rs
-------------
* generate /etc/network/interfaces config to set ip-addresses
* auto-generate dummy interface for every fabric
pve-manager
-------------
* simplify a lot
* remove interface entries in tree
* hide specific openfabric/ospf options (hello-interval, passive etc.)
frr (external)
--------------
* fix --dummy_as_loopback bug (already on staging)
RFC
===
Changelog v2:
=============
proxmox-ve-rs
-------------
* serialize internal representation directly to the frr format
* add integration tests to proxmox-frr
* change internal representation to use BTreeMap instead of HashMap (so that
the test output is ordered)
* move some stuff from proxmox-frr and proxmox-ve-config to proxmox-network-types
pve-network
-----------
* generate frr config and append to running config directly (without going
through perl frr merging)
* check permissions on each fabric when listing
pve-manager
-----------
* autogenerate net and router-id when selecting the first interface
pve-cluster
-----------
* update the config files in status.c (pve-cluster) (thanks @Thomas)
frr (external)
--------------
* got this one merged: https://github.com/FRRouting/frr/pull/18242, so we
*could* automatically add dummy interfaces
Big thanks to Gabriel Goller for his help and support throughout this series!
proxmox:
Stefan Hanreich (4):
network-types: initial commit
network-types: make cidr and mac-address types usable by the api
network-types: add api types for ipv4/6
api-macro: add allof schema to enum
Cargo.toml | 2 +
proxmox-api-macro/src/api/enums.rs | 1 +
proxmox-network-types/Cargo.toml | 22 +
proxmox-network-types/debian/changelog | 5 +
proxmox-network-types/debian/copyright | 18 +
proxmox-network-types/debian/debcargo.toml | 7 +
proxmox-network-types/src/ip_address.rs | 1572 ++++++++++++++++++++
proxmox-network-types/src/lib.rs | 5 +
proxmox-network-types/src/mac_address.rs | 146 ++
9 files changed, 1778 insertions(+)
create mode 100644 proxmox-network-types/Cargo.toml
create mode 100644 proxmox-network-types/debian/changelog
create mode 100644 proxmox-network-types/debian/copyright
create mode 100644 proxmox-network-types/debian/debcargo.toml
create mode 100644 proxmox-network-types/src/ip_address.rs
create mode 100644 proxmox-network-types/src/lib.rs
create mode 100644 proxmox-network-types/src/mac_address.rs
proxmox-firewall:
Stefan Hanreich (1):
firewall: nftables: migrate to proxmox-network-types
Cargo.toml | 1 +
proxmox-firewall/Cargo.toml | 1 +
proxmox-firewall/src/firewall.rs | 8 ++++----
proxmox-firewall/src/object.rs | 4 +++-
proxmox-firewall/src/rule.rs | 3 ++-
proxmox-nftables/Cargo.toml | 3 ++-
proxmox-nftables/src/expression.rs | 7 +++----
proxmox-nftables/src/types.rs | 2 +-
8 files changed, 17 insertions(+), 12 deletions(-)
proxmox-ve-rs:
Gabriel Goller (7):
frr: create proxmox-frr crate
frr: add common frr types
frr: add openfabric types
frr: add ospf types
frr: add route-map types
frr: add generic types over openfabric and ospf
ve-config: add integrations tests
Stefan Hanreich (14):
config: use proxmox_serde perl helpers
ve-config: move types to proxmox-network-types
sdn-types: initial commit
config: sdn: fabrics: add section types
config: sdn: fabrics: add node section types
config: sdn: fabrics: add interface name struct
config: sdn: fabrics: add openfabric properties
config: sdn: fabrics: add ospf properties
config: sdn: fabrics: add api types
config: sdn: fabrics: add section config
config: sdn: fabrics: add fabric config
common: sdn: fabrics: implement validation
sdn: fabrics: config: add conversion from / to section config
sdn: fabrics: implement FRR configuration generation
Cargo.toml | 14 +
proxmox-frr/Cargo.toml | 23 +
proxmox-frr/debian/changelog | 5 +
proxmox-frr/debian/control | 49 +
proxmox-frr/debian/copyright | 18 +
proxmox-frr/debian/debcargo.toml | 7 +
proxmox-frr/src/lib.rs | 231 +++
proxmox-frr/src/openfabric.rs | 114 ++
proxmox-frr/src/ospf.rs | 179 +++
proxmox-frr/src/route_map.rs | 233 +++
proxmox-frr/src/serializer.rs | 203 +++
proxmox-sdn-types/Cargo.toml | 19 +
proxmox-sdn-types/debian/changelog | 5 +
proxmox-sdn-types/debian/control | 53 +
proxmox-sdn-types/debian/copyright | 18 +
proxmox-sdn-types/debian/debcargo.toml | 7 +
proxmox-sdn-types/src/area.rs | 50 +
proxmox-sdn-types/src/lib.rs | 3 +
proxmox-sdn-types/src/net.rs | 329 ++++
proxmox-sdn-types/src/openfabric.rs | 72 +
proxmox-ve-config/Cargo.toml | 24 +-
proxmox-ve-config/debian/control | 41 +-
proxmox-ve-config/src/common/mod.rs | 2 +
proxmox-ve-config/src/common/valid.rs | 53 +
proxmox-ve-config/src/firewall/bridge.rs | 3 +-
proxmox-ve-config/src/firewall/cluster.rs | 9 +-
proxmox-ve-config/src/firewall/ct_helper.rs | 2 +-
proxmox-ve-config/src/firewall/guest.rs | 14 +-
proxmox-ve-config/src/firewall/host.rs | 30 +-
proxmox-ve-config/src/firewall/parse.rs | 80 -
.../src/firewall/types/address.rs | 1394 +----------------
proxmox-ve-config/src/firewall/types/alias.rs | 2 +-
proxmox-ve-config/src/firewall/types/ipset.rs | 8 +-
proxmox-ve-config/src/firewall/types/mod.rs | 1 -
proxmox-ve-config/src/firewall/types/rule.rs | 5 +-
.../src/firewall/types/rule_match.rs | 6 +-
proxmox-ve-config/src/guest/vm.rs | 96 +-
proxmox-ve-config/src/host/utils.rs | 2 +-
proxmox-ve-config/src/sdn/config.rs | 9 +-
proxmox-ve-config/src/sdn/fabric/frr.rs | 390 +++++
proxmox-ve-config/src/sdn/fabric/mod.rs | 732 +++++++++
.../src/sdn/fabric/section_config/fabric.rs | 256 +++
.../sdn/fabric/section_config/interface.rs | 22 +
.../src/sdn/fabric/section_config/mod.rs | 109 ++
.../src/sdn/fabric/section_config/node.rs | 397 +++++
.../sdn/fabric/section_config/protocol/mod.rs | 2 +
.../section_config/protocol/openfabric.rs | 141 ++
.../fabric/section_config/protocol/ospf.rs | 130 ++
proxmox-ve-config/src/sdn/frr.rs | 42 +
proxmox-ve-config/src/sdn/ipam.rs | 11 +-
proxmox-ve-config/src/sdn/mod.rs | 6 +-
.../fabric/cfg/openfabric_default/fabrics.cfg | 18 +
.../cfg/openfabric_dualstack/fabrics.cfg | 22 +
.../cfg/openfabric_ipv6_only/fabrics.cfg | 18 +
.../cfg/openfabric_loopback/fabrics.cfg | 18 +
.../fabrics.cfg | 25 +
.../cfg/openfabric_multi_fabric/fabrics.cfg | 25 +
.../fabrics.cfg | 25 +
.../openfabric_verification_fail/fabrics.cfg | 12 +
.../tests/fabric/cfg/ospf_default/fabrics.cfg | 13 +
.../cfg/ospf_loopback_prefix_fail/fabrics.cfg | 17 +
.../fabric/cfg/ospf_multi_fabric/fabrics.cfg | 25 +
.../cfg/ospf_verification_fail/fabrics.cfg | 13 +
proxmox-ve-config/tests/fabric/helper.rs | 43 +
proxmox-ve-config/tests/fabric/main.rs | 141 ++
.../fabric__openfabric_default_pve.snap | 34 +
.../fabric__openfabric_default_pve1.snap | 33 +
.../fabric__openfabric_dualstack_pve.snap | 46 +
.../fabric__openfabric_ipv6_only_pve.snap | 34 +
.../fabric__openfabric_multi_fabric_pve1.snap | 49 +
.../snapshots/fabric__ospf_default_pve.snap | 32 +
.../snapshots/fabric__ospf_default_pve1.snap | 28 +
.../fabric__ospf_multi_fabric_pve1.snap | 45 +
proxmox-ve-config/tests/sdn/main.rs | 11 +-
74 files changed, 4765 insertions(+), 1613 deletions(-)
create mode 100644 proxmox-frr/Cargo.toml
create mode 100644 proxmox-frr/debian/changelog
create mode 100644 proxmox-frr/debian/control
create mode 100644 proxmox-frr/debian/copyright
create mode 100644 proxmox-frr/debian/debcargo.toml
create mode 100644 proxmox-frr/src/lib.rs
create mode 100644 proxmox-frr/src/openfabric.rs
create mode 100644 proxmox-frr/src/ospf.rs
create mode 100644 proxmox-frr/src/route_map.rs
create mode 100644 proxmox-frr/src/serializer.rs
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/area.rs
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
create mode 100644 proxmox-ve-config/src/common/valid.rs
create mode 100644 proxmox-ve-config/src/sdn/fabric/frr.rs
create mode 100644 proxmox-ve-config/src/sdn/fabric/mod.rs
create mode 100644 proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
create mode 100644 proxmox-ve-config/src/sdn/fabric/section_config/interface.rs
create mode 100644 proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
create mode 100644 proxmox-ve-config/src/sdn/fabric/section_config/node.rs
create mode 100644 proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs
create mode 100644 proxmox-ve-config/src/sdn/fabric/section_config/protocol/openfabric.rs
create mode 100644 proxmox-ve-config/src/sdn/fabric/section_config/protocol/ospf.rs
create mode 100644 proxmox-ve-config/src/sdn/frr.rs
create mode 100644 proxmox-ve-config/tests/fabric/cfg/openfabric_default/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/cfg/openfabric_dualstack/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/cfg/openfabric_ipv6_only/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/cfg/openfabric_loopback/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/cfg/openfabric_loopback_prefix_fail/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/cfg/openfabric_multi_fabric/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/cfg/openfabric_same_net_on_same_node/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/cfg/openfabric_verification_fail/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/cfg/ospf_default/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/cfg/ospf_loopback_prefix_fail/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/cfg/ospf_multi_fabric/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/cfg/ospf_verification_fail/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/helper.rs
create mode 100644 proxmox-ve-config/tests/fabric/main.rs
create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve.snap
create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve1.snap
create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_dualstack_pve.snap
create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_ipv6_only_pve.snap
create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_multi_fabric_pve1.snap
create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve.snap
create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve1.snap
create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_multi_fabric_pve1.snap
proxmox-perl-rs:
Stefan Hanreich (5):
pve-rs: Add PVE::RS::SDN::Fabrics module
pve-rs: sdn: fabrics: add api methods
pve-rs: sdn: fabrics: add frr config generation
pve-rs: sdn: fabrics: add helper to generate ifupdown2 configuration
pve-rs: sdn: fabrics: add helper for network API endpoint
pve-rs/Cargo.toml | 5 +-
pve-rs/Makefile | 1 +
pve-rs/debian/control | 2 +
pve-rs/src/bindings/mod.rs | 3 +
pve-rs/src/bindings/sdn/fabrics.rs | 506 +++++++++++++++++++++++++++++
pve-rs/src/bindings/sdn/mod.rs | 1 +
6 files changed, 517 insertions(+), 1 deletion(-)
create mode 100644 pve-rs/src/bindings/sdn/fabrics.rs
create mode 100644 pve-rs/src/bindings/sdn/mod.rs
pve-cluster:
Stefan Hanreich (1):
cfs: add fabrics.cfg to observed files
debian/pve-cluster.postinst | 24 ++++++++++++++++++++++++
src/PVE/Cluster.pm | 3 +--
src/pmxcfs/status.c | 3 +--
3 files changed, 26 insertions(+), 4 deletions(-)
create mode 100644 debian/pve-cluster.postinst
pve-access-control:
Stefan Hanreich (1):
permissions: add ACL paths for SDN fabrics
src/PVE/AccessControl.pm | 2 ++
1 file changed, 2 insertions(+)
pve-network:
Gabriel Goller (1):
debian: add dependency to proxmox-perl-rs
Stefan Hanreich (20):
sdn: fix value returned by pending_config
fabrics: add fabrics module
refactor: controller: move frr methods into helper
frr: add new helpers for reloading frr configuration
controllers: define new api for frr config generation
sdn: add frr config generation helpers
sdn: api: add check for rewriting frr configuration
test: isis: add test for standalone configuration
sdn: frr: add daemon status to frr helper
sdn: commit fabrics config to running configuration
fabrics: generate ifupdown configuration
fabrics: add jsonschema for fabrics and nodes
api: fabrics: add root-level module
api: fabrics: add fabric submodule
api: fabrics: add node submodule
api: fabrics: add fabricnode submodule
controller: evpn: add fabrics integration
zone: vxlan: add fabrics integration
test: fabrics: add test cases for ospf and openfabric + evpn
frr: bump frr config version to 10.2.2
debian/control | 17 +-
src/PVE/API2/Network/SDN.pm | 22 +-
src/PVE/API2/Network/SDN/Fabrics.pm | 180 +++++++
src/PVE/API2/Network/SDN/Fabrics/Fabric.pm | 248 ++++++++++
.../API2/Network/SDN/Fabrics/FabricNode.pm | 242 +++++++++
src/PVE/API2/Network/SDN/Fabrics/Makefile | 8 +
src/PVE/API2/Network/SDN/Fabrics/Node.pm | 113 +++++
src/PVE/API2/Network/SDN/Makefile | 3 +-
src/PVE/Network/SDN.pm | 158 +++++-
src/PVE/Network/SDN/Controllers.pm | 67 +--
src/PVE/Network/SDN/Controllers/BgpPlugin.pm | 21 +-
src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 441 +++++------------
src/PVE/Network/SDN/Controllers/IsisPlugin.pm | 21 +-
src/PVE/Network/SDN/Controllers/Plugin.pm | 31 +-
src/PVE/Network/SDN/Fabrics.pm | 290 +++++++++++
src/PVE/Network/SDN/Frr.pm | 465 ++++++++++++++++++
src/PVE/Network/SDN/Makefile | 2 +-
src/PVE/Network/SDN/Zones.pm | 10 -
src/PVE/Network/SDN/Zones/EvpnPlugin.pm | 56 ++-
src/PVE/Network/SDN/Zones/VxlanPlugin.pm | 58 ++-
src/test/run_test_zones.pl | 11 +-
.../expected_controller_config | 2 +-
.../expected_controller_config | 2 +-
.../evpn/ebgp/expected_controller_config | 2 +-
.../ebgp_loopback/expected_controller_config | 2 +-
.../evpn/exitnode/expected_controller_config | 2 +-
.../expected_controller_config | 2 +-
.../expected_controller_config | 2 +-
.../exitnode_snat/expected_controller_config | 2 +-
.../expected_controller_config | 2 +-
.../evpn/ipv4/expected_controller_config | 2 +-
.../evpn/ipv4ipv6/expected_controller_config | 2 +-
.../expected_controller_config | 2 +-
.../evpn/ipv6/expected_controller_config | 2 +-
.../ipv6underlay/expected_controller_config | 2 +-
.../evpn/isis/expected_controller_config | 2 +-
.../isis_loopback/expected_controller_config | 2 +-
.../expected_controller_config | 22 +
.../isis_standalone/expected_sdn_interfaces | 1 +
.../zones/evpn/isis_standalone/interfaces | 12 +
.../zones/evpn/isis_standalone/sdn_config | 21 +
.../expected_controller_config | 2 +-
.../multiplezones/expected_controller_config | 2 +-
.../expected_controller_config | 74 +++
.../openfabric_fabric/expected_sdn_interfaces | 56 +++
.../zones/evpn/openfabric_fabric/interfaces | 6 +
.../zones/evpn/openfabric_fabric/sdn_config | 79 +++
.../ospf_fabric/expected_controller_config | 68 +++
.../evpn/ospf_fabric/expected_sdn_interfaces | 53 ++
src/test/zones/evpn/ospf_fabric/interfaces | 6 +
src/test/zones/evpn/ospf_fabric/sdn_config | 76 +++
.../evpn/rt_import/expected_controller_config | 2 +-
.../evpn/vxlanport/expected_controller_config | 2 +-
53 files changed, 2454 insertions(+), 524 deletions(-)
create mode 100644 src/PVE/API2/Network/SDN/Fabrics.pm
create mode 100644 src/PVE/API2/Network/SDN/Fabrics/Fabric.pm
create mode 100644 src/PVE/API2/Network/SDN/Fabrics/FabricNode.pm
create mode 100644 src/PVE/API2/Network/SDN/Fabrics/Makefile
create mode 100644 src/PVE/API2/Network/SDN/Fabrics/Node.pm
create mode 100644 src/PVE/Network/SDN/Fabrics.pm
create mode 100644 src/PVE/Network/SDN/Frr.pm
create mode 100644 src/test/zones/evpn/isis_standalone/expected_controller_config
create mode 100644 src/test/zones/evpn/isis_standalone/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/isis_standalone/interfaces
create mode 100644 src/test/zones/evpn/isis_standalone/sdn_config
create mode 100644 src/test/zones/evpn/openfabric_fabric/expected_controller_config
create mode 100644 src/test/zones/evpn/openfabric_fabric/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/openfabric_fabric/interfaces
create mode 100644 src/test/zones/evpn/openfabric_fabric/sdn_config
create mode 100644 src/test/zones/evpn/ospf_fabric/expected_controller_config
create mode 100644 src/test/zones/evpn/ospf_fabric/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/ospf_fabric/interfaces
create mode 100644 src/test/zones/evpn/ospf_fabric/sdn_config
proxmox-widget-toolkit:
Stefan Hanreich (1):
network selector: add type parameter
src/form/NetworkSelector.js | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
pve-manager:
Gabriel Goller (3):
api: use new sdn config generation functions
fabrics: Add main FabricView
utils: avoid line-break in pending changes message
Stefan Hanreich (15):
ui: fabrics: add model definitions for fabrics
fabric: add common interface panel
fabric: add OpenFabric interface properties
fabric: add OSPF interface properties
fabric: add generic node edit panel
fabric: add OpenFabric node edit
fabric: add OSPF node edit
fabric: add generic fabric edit panel
fabric: add OpenFabric fabric edit panel
fabric: add OSPF fabric edit panel
ui: permissions: add ACL path for fabrics
api: network: add include_sdn / fabric type
ui: add sdn networks to ceph / migration
ui: sdn: add evpn controller fabric integration
ui: sdn: vxlan: add fabric property
PVE/API2/Network.pm | 56 ++-
www/manager6/Makefile | 11 +
www/manager6/Utils.js | 2 +-
www/manager6/ceph/CephInstallWizard.js | 2 +
www/manager6/data/PermPathStore.js | 1 +
www/manager6/dc/Config.js | 8 +
www/manager6/dc/OptionView.js | 1 +
www/manager6/sdn/FabricsView.js | 464 ++++++++++++++++++
www/manager6/sdn/controllers/Base.js | 17 +
www/manager6/sdn/controllers/EvpnEdit.js | 35 +-
www/manager6/sdn/fabrics/Common.js | 36 ++
www/manager6/sdn/fabrics/FabricEdit.js | 57 +++
www/manager6/sdn/fabrics/InterfacePanel.js | 220 +++++++++
www/manager6/sdn/fabrics/NodeEdit.js | 224 +++++++++
.../sdn/fabrics/openfabric/FabricEdit.js | 47 ++
.../sdn/fabrics/openfabric/InterfacePanel.js | 34 ++
.../sdn/fabrics/openfabric/NodeEdit.js | 22 +
www/manager6/sdn/fabrics/ospf/FabricEdit.js | 20 +
.../sdn/fabrics/ospf/InterfacePanel.js | 3 +
www/manager6/sdn/fabrics/ospf/NodeEdit.js | 8 +
www/manager6/sdn/zones/VxlanEdit.js | 52 +-
21 files changed, 1303 insertions(+), 17 deletions(-)
create mode 100644 www/manager6/sdn/FabricsView.js
create mode 100644 www/manager6/sdn/fabrics/Common.js
create mode 100644 www/manager6/sdn/fabrics/FabricEdit.js
create mode 100644 www/manager6/sdn/fabrics/InterfacePanel.js
create mode 100644 www/manager6/sdn/fabrics/NodeEdit.js
create mode 100644 www/manager6/sdn/fabrics/openfabric/FabricEdit.js
create mode 100644 www/manager6/sdn/fabrics/openfabric/InterfacePanel.js
create mode 100644 www/manager6/sdn/fabrics/openfabric/NodeEdit.js
create mode 100644 www/manager6/sdn/fabrics/ospf/FabricEdit.js
create mode 100644 www/manager6/sdn/fabrics/ospf/InterfacePanel.js
create mode 100644 www/manager6/sdn/fabrics/ospf/NodeEdit.js
pve-gui-tests:
Gabriel Goller (1):
pve: add sdn/fabrics screenshots
create_fabrics_screenshots | 198 +++++++++++++++++++++++++++++++++++++
1 file changed, 198 insertions(+)
create mode 100755 create_fabrics_screenshots
pve-docs:
Gabriel Goller (1):
fabrics: add initial documentation for sdn fabrics
pvesdn.adoc | 227 ++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 227 insertions(+)
Summary over all repositories:
178 files changed, 11299 insertions(+), 2173 deletions(-)
--
Generated by git-murpp 0.8.0
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox v3 1/4] network-types: initial commit
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox v3 2/4] network-types: make cidr and mac-address types usable by the api Stefan Hanreich
` (73 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
This commit moves some IP address and MAC address types from
proxmox-ve-config to proxmox, so they can be used re-used across our
code base.
The code in this commit is mostly the same as in proxmox-ve-config
('bc9253d8'), but I have made a few changes:
* Added additional documentation to some of the structs and their
methods
* Converted all error types to thiserror
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
Cargo.toml | 2 +
proxmox-network-types/Cargo.toml | 21 +
proxmox-network-types/debian/changelog | 5 +
proxmox-network-types/debian/copyright | 18 +
proxmox-network-types/debian/debcargo.toml | 7 +
proxmox-network-types/src/ip_address.rs | 1410 ++++++++++++++++++++
proxmox-network-types/src/lib.rs | 5 +
proxmox-network-types/src/mac_address.rs | 121 ++
8 files changed, 1589 insertions(+)
create mode 100644 proxmox-network-types/Cargo.toml
create mode 100644 proxmox-network-types/debian/changelog
create mode 100644 proxmox-network-types/debian/copyright
create mode 100644 proxmox-network-types/debian/debcargo.toml
create mode 100644 proxmox-network-types/src/ip_address.rs
create mode 100644 proxmox-network-types/src/lib.rs
create mode 100644 proxmox-network-types/src/mac_address.rs
diff --git a/Cargo.toml b/Cargo.toml
index 71763c5a..56bb9e2b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -24,6 +24,7 @@ members = [
"proxmox-login",
"proxmox-metrics",
"proxmox-network-api",
+ "proxmox-network-types",
"proxmox-notify",
"proxmox-openid",
"proxmox-product-config",
@@ -134,6 +135,7 @@ proxmox-io = { version = "1.1.0", path = "proxmox-io" }
proxmox-lang = { version = "1.3", path = "proxmox-lang" }
proxmox-log= { version = "0.2.9", path = "proxmox-log" }
proxmox-login = { version = "0.2.0", path = "proxmox-login" }
+proxmox-network-types = { version = "0.1.0", path = "proxmox-network-types" }
proxmox-product-config = { version = "0.2.0", path = "proxmox-product-config" }
proxmox-config-digest = { version = "0.1.0", path = "proxmox-config-digest" }
proxmox-rest-server = { version = "0.8.8", path = "proxmox-rest-server" }
diff --git a/proxmox-network-types/Cargo.toml b/proxmox-network-types/Cargo.toml
new file mode 100644
index 00000000..aa6d66bc
--- /dev/null
+++ b/proxmox-network-types/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "proxmox-network-types"
+description = "Rust types for common networking entities"
+version = "0.1.0"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+homepage.workspace = true
+exclude.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+regex = { workspace = true, optional = true}
+serde = { workspace = true, features = [ "derive", "std" ] }
+serde_with = "3.8.1"
+thiserror = "2"
+
+proxmox-schema = { workspace = true, features = [ "api-macro", "api-types" ], optional = true}
+
+[features]
+default = []
diff --git a/proxmox-network-types/debian/changelog b/proxmox-network-types/debian/changelog
new file mode 100644
index 00000000..78cb0ab0
--- /dev/null
+++ b/proxmox-network-types/debian/changelog
@@ -0,0 +1,5 @@
+rust-proxmox-network-types (0.1.0-1) unstable; urgency=medium
+
+ * Initial release.
+
+ -- Proxmox Support Team <support@proxmox.com> Mon, 03 Jun 2024 10:51:11 +0200
diff --git a/proxmox-network-types/debian/copyright b/proxmox-network-types/debian/copyright
new file mode 100644
index 00000000..1ea8a56b
--- /dev/null
+++ b/proxmox-network-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@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-network-types/debian/debcargo.toml b/proxmox-network-types/debian/debcargo.toml
new file mode 100644
index 00000000..b7864cdb
--- /dev/null
+++ b/proxmox-network-types/debian/debcargo.toml
@@ -0,0 +1,7 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@proxmox.com>"
+
+[source]
+vcs_git = "git://git.proxmox.com/git/proxmox.git"
+vcs_browser = "https://git.proxmox.com/?p=proxmox.git"
diff --git a/proxmox-network-types/src/ip_address.rs b/proxmox-network-types/src/ip_address.rs
new file mode 100644
index 00000000..355547b1
--- /dev/null
+++ b/proxmox-network-types/src/ip_address.rs
@@ -0,0 +1,1410 @@
+//! Provides helpers to deal with IP addresses / CIDRs
+
+use std::net::{AddrParseError, IpAddr, Ipv4Addr, Ipv6Addr};
+
+use serde_with::{DeserializeFromStr, SerializeDisplay};
+use thiserror::Error;
+
+/// The family (v4 or v6) of an IP address or CIDR prefix
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum Family {
+ V4,
+ V6,
+}
+
+impl Family {
+ pub fn is_ipv4(&self) -> bool {
+ *self == Self::V4
+ }
+
+ pub fn is_ipv6(&self) -> bool {
+ *self == Self::V6
+ }
+}
+
+impl std::fmt::Display for Family {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match self {
+ Family::V4 => f.write_str("Ipv4"),
+ Family::V6 => f.write_str("Ipv6"),
+ }
+ }
+}
+
+#[derive(Error, Debug)]
+pub enum CidrError {
+ #[error("invalid netmask")]
+ InvalidNetmask,
+ #[error("invalid IP address")]
+ InvalidAddress(#[from] AddrParseError),
+}
+
+/// Represents either an [`Ipv4Cidr`] or [`Ipv6Cidr`] CIDR prefix
+#[derive(
+ Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Hash, SerializeDisplay, DeserializeFromStr,
+)]
+pub enum Cidr {
+ Ipv4(Ipv4Cidr),
+ Ipv6(Ipv6Cidr),
+}
+
+impl Cidr {
+ pub fn new_v4(addr: impl Into<Ipv4Addr>, mask: u8) -> Result<Self, CidrError> {
+ Ok(Cidr::Ipv4(Ipv4Cidr::new(addr, mask)?))
+ }
+
+ pub fn new_v6(addr: impl Into<Ipv6Addr>, mask: u8) -> Result<Self, CidrError> {
+ Ok(Cidr::Ipv6(Ipv6Cidr::new(addr, mask)?))
+ }
+
+ /// which [`Family`] this CIDR belongs to
+ pub const fn family(&self) -> Family {
+ match self {
+ Cidr::Ipv4(_) => Family::V4,
+ Cidr::Ipv6(_) => Family::V6,
+ }
+ }
+
+ pub fn is_ipv4(&self) -> bool {
+ matches!(self, Cidr::Ipv4(_))
+ }
+
+ pub fn is_ipv6(&self) -> bool {
+ matches!(self, Cidr::Ipv6(_))
+ }
+
+ /// Whether a given IP address is contained in this [`Cidr`]
+ ///
+ /// This only works if both [`IpAddr`] are in the same family, otherwise the function returns
+ /// false.
+ pub fn contains_address(&self, ip: &IpAddr) -> bool {
+ match (self, ip) {
+ (Cidr::Ipv4(cidr), IpAddr::V4(ip)) => cidr.contains_address(ip),
+ (Cidr::Ipv6(cidr), IpAddr::V6(ip)) => cidr.contains_address(ip),
+ _ => false,
+ }
+ }
+}
+
+impl std::fmt::Display for Cidr {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match self {
+ Self::Ipv4(ip) => f.write_str(ip.to_string().as_str()),
+ Self::Ipv6(ip) => f.write_str(ip.to_string().as_str()),
+ }
+ }
+}
+
+impl std::str::FromStr for Cidr {
+ type Err = CidrError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ if let Ok(ip) = s.parse::<Ipv4Cidr>() {
+ return Ok(Cidr::Ipv4(ip));
+ }
+
+ Ok(Cidr::Ipv6(s.parse()?))
+ }
+}
+
+impl From<Ipv4Cidr> for Cidr {
+ fn from(cidr: Ipv4Cidr) -> Self {
+ Cidr::Ipv4(cidr)
+ }
+}
+
+impl From<Ipv6Cidr> for Cidr {
+ fn from(cidr: Ipv6Cidr) -> Self {
+ Cidr::Ipv6(cidr)
+ }
+}
+
+impl From<IpAddr> for Cidr {
+ fn from(value: IpAddr) -> Self {
+ match value {
+ IpAddr::V4(addr) => Ipv4Cidr::from(addr).into(),
+ IpAddr::V6(addr) => Ipv6Cidr::from(addr).into(),
+ }
+ }
+}
+
+const IPV4_LENGTH: u8 = 32;
+
+/// An IPv4 CIDR (e.g. 192.0.2.0/24)
+#[derive(
+ SerializeDisplay, DeserializeFromStr, Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Hash,
+)]
+pub struct Ipv4Cidr {
+ addr: Ipv4Addr,
+ mask: u8,
+}
+
+impl Ipv4Cidr {
+ pub fn new(addr: impl Into<Ipv4Addr>, mask: u8) -> Result<Self, CidrError> {
+ if mask > IPV4_LENGTH {
+ return Err(CidrError::InvalidNetmask);
+ }
+
+ Ok(Self {
+ addr: addr.into(),
+ mask,
+ })
+ }
+
+ /// checks whether this CIDR contains an IPv4 address.
+ pub fn contains_address(&self, other: &Ipv4Addr) -> bool {
+ let bits = u32::from_be_bytes(self.addr.octets());
+ let other_bits = u32::from_be_bytes(other.octets());
+
+ let shift_amount: u32 = IPV4_LENGTH.saturating_sub(self.mask).into();
+
+ bits.checked_shr(shift_amount).unwrap_or(0)
+ == other_bits.checked_shr(shift_amount).unwrap_or(0)
+ }
+
+ pub fn address(&self) -> &Ipv4Addr {
+ &self.addr
+ }
+
+ pub fn mask(&self) -> u8 {
+ self.mask
+ }
+}
+
+impl<T: Into<Ipv4Addr>> From<T> for Ipv4Cidr {
+ fn from(value: T) -> Self {
+ Self {
+ addr: value.into(),
+ mask: 32,
+ }
+ }
+}
+
+impl std::str::FromStr for Ipv4Cidr {
+ type Err = CidrError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Ok(match s.find('/') {
+ None => Self {
+ addr: s.parse()?,
+ mask: 32,
+ },
+ Some(pos) => {
+ let mask: u8 = s[(pos + 1)..]
+ .parse()
+ .map_err(|_| CidrError::InvalidNetmask)?;
+
+ Self::new(s[..pos].parse::<Ipv4Addr>()?, mask)?
+ }
+ })
+ }
+}
+
+impl std::fmt::Display for Ipv4Cidr {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "{}/{}", &self.addr, self.mask)
+ }
+}
+
+const IPV6_LENGTH: u8 = 128;
+
+/// An IPv6 CIDR (e.g. 2001:db8::/32)
+#[derive(
+ SerializeDisplay, DeserializeFromStr, Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Hash,
+)]
+pub struct Ipv6Cidr {
+ addr: Ipv6Addr,
+ mask: u8,
+}
+
+impl Ipv6Cidr {
+ pub fn new(addr: impl Into<Ipv6Addr>, mask: u8) -> Result<Self, CidrError> {
+ if mask > IPV6_LENGTH {
+ return Err(CidrError::InvalidNetmask);
+ }
+
+ Ok(Self {
+ addr: addr.into(),
+ mask,
+ })
+ }
+
+ /// checks whether this CIDR contains a given IPv6 address
+ pub fn contains_address(&self, other: &Ipv6Addr) -> bool {
+ let bits = u128::from_be_bytes(self.addr.octets());
+ let other_bits = u128::from_be_bytes(other.octets());
+
+ let shift_amount: u32 = IPV6_LENGTH.saturating_sub(self.mask).into();
+
+ bits.checked_shr(shift_amount).unwrap_or(0)
+ == other_bits.checked_shr(shift_amount).unwrap_or(0)
+ }
+
+ pub fn address(&self) -> &Ipv6Addr {
+ &self.addr
+ }
+
+ pub fn mask(&self) -> u8 {
+ self.mask
+ }
+}
+
+impl std::str::FromStr for Ipv6Cidr {
+ type Err = CidrError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Ok(match s.find('/') {
+ None => Self {
+ addr: s.parse()?,
+ mask: 128,
+ },
+ Some(pos) => {
+ let mask: u8 = s[(pos + 1)..]
+ .parse()
+ .map_err(|_| CidrError::InvalidNetmask)?;
+
+ Self::new(s[..pos].parse::<Ipv6Addr>()?, mask)?
+ }
+ })
+ }
+}
+
+impl std::fmt::Display for Ipv6Cidr {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "{}/{}", &self.addr, self.mask)
+ }
+}
+
+impl<T: Into<Ipv6Addr>> From<T> for Ipv6Cidr {
+ fn from(addr: T) -> Self {
+ Self {
+ addr: addr.into(),
+ mask: 128,
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Error)]
+pub enum IpRangeError {
+ #[error("mismatched ip address families")]
+ MismatchedFamilies,
+ #[error("start is greater than last")]
+ StartGreaterThanLast,
+ #[error("invalid ip range format")]
+ InvalidFormat,
+}
+
+/// Represents a range of IPv4 or IPv6 addresses.
+///
+/// For more information see [`AddressRange`]
+#[derive(
+ Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, SerializeDisplay, DeserializeFromStr,
+)]
+pub enum IpRange {
+ V4(AddressRange<Ipv4Addr>),
+ V6(AddressRange<Ipv6Addr>),
+}
+
+impl IpRange {
+ /// Returns the family of the IpRange.
+ pub fn family(&self) -> Family {
+ match self {
+ IpRange::V4(_) => Family::V4,
+ IpRange::V6(_) => Family::V6,
+ }
+ }
+
+ /// Creates a new [`IpRange`] from two [`IpAddr`].
+ ///
+ /// # Errors
+ ///
+ /// This function will return an error if start and last IP address are not from the same family.
+ pub fn new(start: impl Into<IpAddr>, last: impl Into<IpAddr>) -> Result<Self, IpRangeError> {
+ match (start.into(), last.into()) {
+ (IpAddr::V4(start), IpAddr::V4(last)) => Self::new_v4(start, last),
+ (IpAddr::V6(start), IpAddr::V6(last)) => Self::new_v6(start, last),
+ _ => Err(IpRangeError::MismatchedFamilies),
+ }
+ }
+
+ /// construct a new IPv4 Range
+ pub fn new_v4(
+ start: impl Into<Ipv4Addr>,
+ last: impl Into<Ipv4Addr>,
+ ) -> Result<Self, IpRangeError> {
+ Ok(IpRange::V4(AddressRange::new_v4(start, last)?))
+ }
+
+ /// construct a new IPv6 Range
+ pub fn new_v6(
+ start: impl Into<Ipv6Addr>,
+ last: impl Into<Ipv6Addr>,
+ ) -> Result<Self, IpRangeError> {
+ Ok(IpRange::V6(AddressRange::new_v6(start, last)?))
+ }
+
+ /// Converts an IpRange into the minimal amount of CIDRs.
+ ///
+ /// see the concrete implementations of [`AddressRange<Ipv4Addr>`] or [`AddressRange<Ipv6Addr>`]
+ /// respectively
+ pub fn to_cidrs(&self) -> Vec<Cidr> {
+ match self {
+ IpRange::V4(range) => range.to_cidrs().into_iter().map(Cidr::from).collect(),
+ IpRange::V6(range) => range.to_cidrs().into_iter().map(Cidr::from).collect(),
+ }
+ }
+}
+
+impl std::str::FromStr for IpRange {
+ type Err = IpRangeError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ if let Ok(range) = s.parse() {
+ return Ok(IpRange::V4(range));
+ }
+
+ if let Ok(range) = s.parse() {
+ return Ok(IpRange::V6(range));
+ }
+
+ Err(IpRangeError::InvalidFormat)
+ }
+}
+
+impl std::fmt::Display for IpRange {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ IpRange::V4(range) => range.fmt(f),
+ IpRange::V6(range) => range.fmt(f),
+ }
+ }
+}
+
+/// Represents a range of IP addresses from start to last.
+///
+/// This type is for encapsulation purposes for the [`IpRange`] enum and should be instantiated via
+/// that enum.
+///
+/// # Invariants
+///
+/// * start and last have the same IP address family
+/// * start is less than or equal to last
+///
+/// # Textual representation
+///
+/// Two IP addresses separated by a hyphen, e.g.: `127.0.0.1-127.0.0.255`
+#[derive(
+ Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, SerializeDisplay, DeserializeFromStr,
+)]
+pub struct AddressRange<T> {
+ start: T,
+ last: T,
+}
+
+impl AddressRange<Ipv4Addr> {
+ pub(crate) fn new_v4(
+ start: impl Into<Ipv4Addr>,
+ last: impl Into<Ipv4Addr>,
+ ) -> Result<AddressRange<Ipv4Addr>, IpRangeError> {
+ let (start, last) = (start.into(), last.into());
+
+ if start > last {
+ return Err(IpRangeError::StartGreaterThanLast);
+ }
+
+ Ok(Self { start, last })
+ }
+
+ /// Returns the minimum amount of CIDRs that exactly represent the range
+ ///
+ /// The idea behind this algorithm is as follows:
+ ///
+ /// Start iterating with current = start of the IP range
+ ///
+ /// Find two netmasks
+ /// * The largest CIDR that the current IP can be the first of
+ /// * The largest CIDR that *only* contains IPs from current - last
+ ///
+ /// Add the smaller of the two CIDRs to our result and current to the first IP that is in
+ /// the range but not in the CIDR we just added. Proceed until we reached the last of the IP
+ /// range.
+ ///
+ pub fn to_cidrs(&self) -> Vec<Ipv4Cidr> {
+ let mut cidrs = Vec::new();
+
+ let mut current = u32::from_be_bytes(self.start.octets());
+ let last = u32::from_be_bytes(self.last.octets());
+
+ if current == last {
+ // valid Ipv4 since netmask is 32
+ cidrs.push(Ipv4Cidr::new(current, 32).unwrap());
+ return cidrs;
+ }
+
+ // special case this, since this is the only possibility of overflow
+ // when calculating delta_min_mask - makes everything a lot easier
+ if current == u32::MIN && last == u32::MAX {
+ // valid Ipv4 since it is `0.0.0.0/0`
+ cidrs.push(Ipv4Cidr::new(current, 0).unwrap());
+ return cidrs;
+ }
+
+ while current <= last {
+ // netmask of largest CIDR that current IP can be the first of
+ // cast is safe, because trailing zeroes can at most be 32
+ let current_max_mask = IPV4_LENGTH - (current.trailing_zeros() as u8);
+
+ // netmask of largest CIDR that *only* contains IPs of the remaining range
+ // is at most 32 due to unwrap_or returning 32 and ilog2 being at most 31
+ let delta_min_mask = ((last - current) + 1) // safe due to special case above
+ .checked_ilog2() // should never occur due to special case, but for good measure
+ .map(|mask| IPV4_LENGTH - mask as u8)
+ .unwrap_or(IPV4_LENGTH);
+
+ // at most 32, due to current/delta being at most 32
+ let netmask = u8::max(current_max_mask, delta_min_mask);
+
+ // netmask is at most 32, therefore safe to unwrap
+ cidrs.push(Ipv4Cidr::new(current, netmask).unwrap());
+
+ let delta = 2u32.saturating_pow((IPV4_LENGTH - netmask).into());
+
+ if let Some(result) = current.checked_add(delta) {
+ current = result
+ } else {
+ // we reached the end of IP address space
+ break;
+ }
+ }
+
+ cidrs
+ }
+}
+
+impl AddressRange<Ipv6Addr> {
+ pub(crate) fn new_v6(
+ start: impl Into<Ipv6Addr>,
+ last: impl Into<Ipv6Addr>,
+ ) -> Result<AddressRange<Ipv6Addr>, IpRangeError> {
+ let (start, last) = (start.into(), last.into());
+
+ if start > last {
+ return Err(IpRangeError::StartGreaterThanLast);
+ }
+
+ Ok(Self { start, last })
+ }
+
+ /// Returns the minimum amount of CIDRs that exactly represent the [`AddressRange`].
+ ///
+ /// This function works analogous to the IPv4 version, please refer to the respective
+ /// documentation of [`AddressRange<Ipv4Addr>`]
+ pub fn to_cidrs(&self) -> Vec<Ipv6Cidr> {
+ let mut cidrs = Vec::new();
+
+ let mut current = u128::from_be_bytes(self.start.octets());
+ let last = u128::from_be_bytes(self.last.octets());
+
+ if current == last {
+ // valid Ipv6 since netmask is 128
+ cidrs.push(Ipv6Cidr::new(current, 128).unwrap());
+ return cidrs;
+ }
+
+ // special case this, since this is the only possibility of overflow
+ // when calculating delta_min_mask - makes everything a lot easier
+ if current == u128::MIN && last == u128::MAX {
+ // valid Ipv6 since it is `::/0`
+ cidrs.push(Ipv6Cidr::new(current, 0).unwrap());
+ return cidrs;
+ }
+
+ while current <= last {
+ // netmask of largest CIDR that current IP can be the first of
+ // cast is safe, because trailing zeroes can at most be 128
+ let current_max_mask = IPV6_LENGTH - (current.trailing_zeros() as u8);
+
+ // netmask of largest CIDR that *only* contains IPs of the remaining range
+ // is at most 128 due to unwrap_or returning 128 and ilog2 being at most 31
+ let delta_min_mask = ((last - current) + 1) // safe due to special case above
+ .checked_ilog2() // should never occur due to special case, but for good measure
+ .map(|mask| IPV6_LENGTH - mask as u8)
+ .unwrap_or(IPV6_LENGTH);
+
+ // at most 128, due to current/delta being at most 128
+ let netmask = u8::max(current_max_mask, delta_min_mask);
+
+ // netmask is at most 128, therefore safe to unwrap
+ cidrs.push(Ipv6Cidr::new(current, netmask).unwrap());
+
+ let delta = 2u128.saturating_pow((IPV6_LENGTH - netmask).into());
+
+ if let Some(result) = current.checked_add(delta) {
+ current = result
+ } else {
+ // we reached the end of IP address space
+ break;
+ }
+ }
+
+ cidrs
+ }
+}
+
+impl<T> AddressRange<T> {
+ /// the first IP address contained in this [`AddressRange`]
+ pub fn start(&self) -> &T {
+ &self.start
+ }
+
+ /// the last IP address contained in this [`AddressRange`]
+ pub fn last(&self) -> &T {
+ &self.last
+ }
+}
+
+impl std::str::FromStr for AddressRange<Ipv4Addr> {
+ type Err = IpRangeError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ if let Some((start, last)) = s.split_once('-') {
+ let start_address = start
+ .parse::<Ipv4Addr>()
+ .map_err(|_| IpRangeError::InvalidFormat)?;
+
+ let last_address = last
+ .parse::<Ipv4Addr>()
+ .map_err(|_| IpRangeError::InvalidFormat)?;
+
+ return Self::new_v4(start_address, last_address);
+ }
+
+ Err(IpRangeError::InvalidFormat)
+ }
+}
+
+impl std::str::FromStr for AddressRange<Ipv6Addr> {
+ type Err = IpRangeError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ if let Some((start, last)) = s.split_once('-') {
+ let start_address = start
+ .parse::<Ipv6Addr>()
+ .map_err(|_| IpRangeError::InvalidFormat)?;
+
+ let last_address = last
+ .parse::<Ipv6Addr>()
+ .map_err(|_| IpRangeError::InvalidFormat)?;
+
+ return Self::new_v6(start_address, last_address);
+ }
+
+ Err(IpRangeError::InvalidFormat)
+ }
+}
+
+impl<T: std::fmt::Display> std::fmt::Display for AddressRange<T> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}-{}", self.start, self.last)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::net::{Ipv4Addr, Ipv6Addr};
+
+ #[test]
+ fn test_v4_cidr() {
+ let mut cidr: Ipv4Cidr = "0.0.0.0/0".parse().expect("valid IPv4 CIDR");
+
+ assert_eq!(cidr.addr, Ipv4Addr::new(0, 0, 0, 0));
+ assert_eq!(cidr.mask, 0);
+
+ assert!(cidr.contains_address(&Ipv4Addr::new(0, 0, 0, 0)));
+ assert!(cidr.contains_address(&Ipv4Addr::new(255, 255, 255, 255)));
+
+ cidr = "192.168.100.1".parse().expect("valid IPv4 CIDR");
+
+ assert_eq!(cidr.addr, Ipv4Addr::new(192, 168, 100, 1));
+ assert_eq!(cidr.mask, 32);
+
+ assert!(cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 1)));
+ assert!(!cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 2)));
+ assert!(!cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 0)));
+
+ cidr = "10.100.5.0/24".parse().expect("valid IPv4 CIDR");
+
+ assert_eq!(cidr.mask, 24);
+
+ assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 0)));
+ assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 1)));
+ assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 100)));
+ assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 255)));
+ assert!(!cidr.contains_address(&Ipv4Addr::new(10, 100, 4, 255)));
+ assert!(!cidr.contains_address(&Ipv4Addr::new(10, 100, 6, 0)));
+
+ "0.0.0.0/-1".parse::<Ipv4Cidr>().unwrap_err();
+ "0.0.0.0/33".parse::<Ipv4Cidr>().unwrap_err();
+ "256.256.256.256/10".parse::<Ipv4Cidr>().unwrap_err();
+
+ "fe80::1/64".parse::<Ipv4Cidr>().unwrap_err();
+ "qweasd".parse::<Ipv4Cidr>().unwrap_err();
+ "".parse::<Ipv4Cidr>().unwrap_err();
+ }
+
+ #[test]
+ fn test_v6_cidr() {
+ let mut cidr: Ipv6Cidr = "abab::1/64".parse().expect("valid IPv6 CIDR");
+
+ assert_eq!(cidr.addr, Ipv6Addr::new(0xABAB, 0, 0, 0, 0, 0, 0, 1));
+ assert_eq!(cidr.mask, 64);
+
+ assert!(cidr.contains_address(&Ipv6Addr::new(0xABAB, 0, 0, 0, 0, 0, 0, 0)));
+ assert!(cidr.contains_address(&Ipv6Addr::new(
+ 0xABAB, 0, 0, 0, 0xAAAA, 0xAAAA, 0xAAAA, 0xAAAA
+ )));
+ assert!(cidr.contains_address(&Ipv6Addr::new(
+ 0xABAB, 0, 0, 0, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
+ )));
+ assert!(!cidr.contains_address(&Ipv6Addr::new(0xABAB, 0, 0, 1, 0, 0, 0, 0)));
+ assert!(!cidr.contains_address(&Ipv6Addr::new(
+ 0xABAA, 0, 0, 0, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
+ )));
+
+ cidr = "eeee::1".parse().expect("valid IPv6 CIDR");
+
+ assert_eq!(cidr.mask, 128);
+
+ assert!(cidr.contains_address(&Ipv6Addr::new(0xEEEE, 0, 0, 0, 0, 0, 0, 1)));
+ assert!(!cidr.contains_address(&Ipv6Addr::new(
+ 0xEEED, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
+ )));
+ assert!(!cidr.contains_address(&Ipv6Addr::new(0xEEEE, 0, 0, 0, 0, 0, 0, 0)));
+
+ "eeee::1/-1".parse::<Ipv6Cidr>().unwrap_err();
+ "eeee::1/129".parse::<Ipv6Cidr>().unwrap_err();
+ "gggg::1/64".parse::<Ipv6Cidr>().unwrap_err();
+
+ "192.168.0.1".parse::<Ipv6Cidr>().unwrap_err();
+ "qweasd".parse::<Ipv6Cidr>().unwrap_err();
+ "".parse::<Ipv6Cidr>().unwrap_err();
+ }
+
+ #[test]
+ fn test_ip_range() {
+ IpRange::new([10, 0, 0, 2], [10, 0, 0, 1]).unwrap_err();
+
+ IpRange::new(
+ [0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x1000],
+ [0x2001, 0x0db8, 0, 0, 0, 0, 0, 0],
+ )
+ .unwrap_err();
+
+ let v4_range = IpRange::new([10, 0, 0, 0], [10, 0, 0, 100]).unwrap();
+ assert_eq!(v4_range.family(), Family::V4);
+
+ let v6_range = IpRange::new(
+ [0x2001, 0x0db8, 0, 0, 0, 0, 0, 0],
+ [0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x1000],
+ )
+ .unwrap();
+ assert_eq!(v6_range.family(), Family::V6);
+
+ "10.0.0.1-10.0.0.100".parse::<IpRange>().unwrap();
+ "2001:db8::1-2001:db8::f".parse::<IpRange>().unwrap();
+
+ "10.0.0.1-2001:db8::1000".parse::<IpRange>().unwrap_err();
+ "2001:db8::1-192.168.0.2".parse::<IpRange>().unwrap_err();
+
+ "10.0.0.1-10.0.0.0".parse::<IpRange>().unwrap_err();
+ "2001:db8::1-2001:db8::0".parse::<IpRange>().unwrap_err();
+ }
+
+ #[test]
+ fn test_ipv4_to_cidrs() {
+ let range = AddressRange::new_v4([192, 168, 0, 100], [192, 168, 0, 100]).unwrap();
+
+ assert_eq!(
+ [Ipv4Cidr::new([192, 168, 0, 100], 32).unwrap()],
+ range.to_cidrs().as_slice()
+ );
+
+ let range = AddressRange::new_v4([192, 168, 0, 100], [192, 168, 0, 200]).unwrap();
+
+ assert_eq!(
+ [
+ Ipv4Cidr::new([192, 168, 0, 100], 30).unwrap(),
+ Ipv4Cidr::new([192, 168, 0, 104], 29).unwrap(),
+ Ipv4Cidr::new([192, 168, 0, 112], 28).unwrap(),
+ Ipv4Cidr::new([192, 168, 0, 128], 26).unwrap(),
+ Ipv4Cidr::new([192, 168, 0, 192], 29).unwrap(),
+ Ipv4Cidr::new([192, 168, 0, 200], 32).unwrap(),
+ ],
+ range.to_cidrs().as_slice()
+ );
+
+ let range = AddressRange::new_v4([192, 168, 0, 101], [192, 168, 0, 200]).unwrap();
+
+ assert_eq!(
+ [
+ Ipv4Cidr::new([192, 168, 0, 101], 32).unwrap(),
+ Ipv4Cidr::new([192, 168, 0, 102], 31).unwrap(),
+ Ipv4Cidr::new([192, 168, 0, 104], 29).unwrap(),
+ Ipv4Cidr::new([192, 168, 0, 112], 28).unwrap(),
+ Ipv4Cidr::new([192, 168, 0, 128], 26).unwrap(),
+ Ipv4Cidr::new([192, 168, 0, 192], 29).unwrap(),
+ Ipv4Cidr::new([192, 168, 0, 200], 32).unwrap(),
+ ],
+ range.to_cidrs().as_slice()
+ );
+
+ let range = AddressRange::new_v4([192, 168, 0, 101], [192, 168, 0, 101]).unwrap();
+
+ assert_eq!(
+ [Ipv4Cidr::new([192, 168, 0, 101], 32).unwrap()],
+ range.to_cidrs().as_slice()
+ );
+
+ let range = AddressRange::new_v4([192, 168, 0, 101], [192, 168, 0, 201]).unwrap();
+
+ assert_eq!(
+ [
+ Ipv4Cidr::new([192, 168, 0, 101], 32).unwrap(),
+ Ipv4Cidr::new([192, 168, 0, 102], 31).unwrap(),
+ Ipv4Cidr::new([192, 168, 0, 104], 29).unwrap(),
+ Ipv4Cidr::new([192, 168, 0, 112], 28).unwrap(),
+ Ipv4Cidr::new([192, 168, 0, 128], 26).unwrap(),
+ Ipv4Cidr::new([192, 168, 0, 192], 29).unwrap(),
+ Ipv4Cidr::new([192, 168, 0, 200], 31).unwrap(),
+ ],
+ range.to_cidrs().as_slice()
+ );
+
+ let range = AddressRange::new_v4([192, 168, 0, 0], [192, 168, 0, 255]).unwrap();
+
+ assert_eq!(
+ [Ipv4Cidr::new([192, 168, 0, 0], 24).unwrap(),],
+ range.to_cidrs().as_slice()
+ );
+
+ let range = AddressRange::new_v4([0, 0, 0, 0], [255, 255, 255, 255]).unwrap();
+
+ assert_eq!(
+ [Ipv4Cidr::new([0, 0, 0, 0], 0).unwrap(),],
+ range.to_cidrs().as_slice()
+ );
+
+ let range = AddressRange::new_v4([0, 0, 0, 1], [255, 255, 255, 255]).unwrap();
+
+ assert_eq!(
+ [
+ Ipv4Cidr::new([0, 0, 0, 1], 32).unwrap(),
+ Ipv4Cidr::new([0, 0, 0, 2], 31).unwrap(),
+ Ipv4Cidr::new([0, 0, 0, 4], 30).unwrap(),
+ Ipv4Cidr::new([0, 0, 0, 8], 29).unwrap(),
+ Ipv4Cidr::new([0, 0, 0, 16], 28).unwrap(),
+ Ipv4Cidr::new([0, 0, 0, 32], 27).unwrap(),
+ Ipv4Cidr::new([0, 0, 0, 64], 26).unwrap(),
+ Ipv4Cidr::new([0, 0, 0, 128], 25).unwrap(),
+ Ipv4Cidr::new([0, 0, 1, 0], 24).unwrap(),
+ Ipv4Cidr::new([0, 0, 2, 0], 23).unwrap(),
+ Ipv4Cidr::new([0, 0, 4, 0], 22).unwrap(),
+ Ipv4Cidr::new([0, 0, 8, 0], 21).unwrap(),
+ Ipv4Cidr::new([0, 0, 16, 0], 20).unwrap(),
+ Ipv4Cidr::new([0, 0, 32, 0], 19).unwrap(),
+ Ipv4Cidr::new([0, 0, 64, 0], 18).unwrap(),
+ Ipv4Cidr::new([0, 0, 128, 0], 17).unwrap(),
+ Ipv4Cidr::new([0, 1, 0, 0], 16).unwrap(),
+ Ipv4Cidr::new([0, 2, 0, 0], 15).unwrap(),
+ Ipv4Cidr::new([0, 4, 0, 0], 14).unwrap(),
+ Ipv4Cidr::new([0, 8, 0, 0], 13).unwrap(),
+ Ipv4Cidr::new([0, 16, 0, 0], 12).unwrap(),
+ Ipv4Cidr::new([0, 32, 0, 0], 11).unwrap(),
+ Ipv4Cidr::new([0, 64, 0, 0], 10).unwrap(),
+ Ipv4Cidr::new([0, 128, 0, 0], 9).unwrap(),
+ Ipv4Cidr::new([1, 0, 0, 0], 8).unwrap(),
+ Ipv4Cidr::new([2, 0, 0, 0], 7).unwrap(),
+ Ipv4Cidr::new([4, 0, 0, 0], 6).unwrap(),
+ Ipv4Cidr::new([8, 0, 0, 0], 5).unwrap(),
+ Ipv4Cidr::new([16, 0, 0, 0], 4).unwrap(),
+ Ipv4Cidr::new([32, 0, 0, 0], 3).unwrap(),
+ Ipv4Cidr::new([64, 0, 0, 0], 2).unwrap(),
+ Ipv4Cidr::new([128, 0, 0, 0], 1).unwrap(),
+ ],
+ range.to_cidrs().as_slice()
+ );
+
+ let range = AddressRange::new_v4([0, 0, 0, 0], [255, 255, 255, 254]).unwrap();
+
+ assert_eq!(
+ [
+ Ipv4Cidr::new([0, 0, 0, 0], 1).unwrap(),
+ Ipv4Cidr::new([128, 0, 0, 0], 2).unwrap(),
+ Ipv4Cidr::new([192, 0, 0, 0], 3).unwrap(),
+ Ipv4Cidr::new([224, 0, 0, 0], 4).unwrap(),
+ Ipv4Cidr::new([240, 0, 0, 0], 5).unwrap(),
+ Ipv4Cidr::new([248, 0, 0, 0], 6).unwrap(),
+ Ipv4Cidr::new([252, 0, 0, 0], 7).unwrap(),
+ Ipv4Cidr::new([254, 0, 0, 0], 8).unwrap(),
+ Ipv4Cidr::new([255, 0, 0, 0], 9).unwrap(),
+ Ipv4Cidr::new([255, 128, 0, 0], 10).unwrap(),
+ Ipv4Cidr::new([255, 192, 0, 0], 11).unwrap(),
+ Ipv4Cidr::new([255, 224, 0, 0], 12).unwrap(),
+ Ipv4Cidr::new([255, 240, 0, 0], 13).unwrap(),
+ Ipv4Cidr::new([255, 248, 0, 0], 14).unwrap(),
+ Ipv4Cidr::new([255, 252, 0, 0], 15).unwrap(),
+ Ipv4Cidr::new([255, 254, 0, 0], 16).unwrap(),
+ Ipv4Cidr::new([255, 255, 0, 0], 17).unwrap(),
+ Ipv4Cidr::new([255, 255, 128, 0], 18).unwrap(),
+ Ipv4Cidr::new([255, 255, 192, 0], 19).unwrap(),
+ Ipv4Cidr::new([255, 255, 224, 0], 20).unwrap(),
+ Ipv4Cidr::new([255, 255, 240, 0], 21).unwrap(),
+ Ipv4Cidr::new([255, 255, 248, 0], 22).unwrap(),
+ Ipv4Cidr::new([255, 255, 252, 0], 23).unwrap(),
+ Ipv4Cidr::new([255, 255, 254, 0], 24).unwrap(),
+ Ipv4Cidr::new([255, 255, 255, 0], 25).unwrap(),
+ Ipv4Cidr::new([255, 255, 255, 128], 26).unwrap(),
+ Ipv4Cidr::new([255, 255, 255, 192], 27).unwrap(),
+ Ipv4Cidr::new([255, 255, 255, 224], 28).unwrap(),
+ Ipv4Cidr::new([255, 255, 255, 240], 29).unwrap(),
+ Ipv4Cidr::new([255, 255, 255, 248], 30).unwrap(),
+ Ipv4Cidr::new([255, 255, 255, 252], 31).unwrap(),
+ Ipv4Cidr::new([255, 255, 255, 254], 32).unwrap(),
+ ],
+ range.to_cidrs().as_slice()
+ );
+
+ let range = AddressRange::new_v4([0, 0, 0, 0], [0, 0, 0, 0]).unwrap();
+
+ assert_eq!(
+ [Ipv4Cidr::new([0, 0, 0, 0], 32).unwrap(),],
+ range.to_cidrs().as_slice()
+ );
+
+ let range = AddressRange::new_v4([255, 255, 255, 255], [255, 255, 255, 255]).unwrap();
+
+ assert_eq!(
+ [Ipv4Cidr::new([255, 255, 255, 255], 32).unwrap(),],
+ range.to_cidrs().as_slice()
+ );
+ }
+
+ #[test]
+ fn test_ipv6_to_cidrs() {
+ let range = AddressRange::new_v6(
+ [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1000],
+ [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1000],
+ )
+ .unwrap();
+
+ assert_eq!(
+ [Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1000], 128).unwrap()],
+ range.to_cidrs().as_slice()
+ );
+
+ let range = AddressRange::new_v6(
+ [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1000],
+ [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x2000],
+ )
+ .unwrap();
+
+ assert_eq!(
+ [
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1000], 116).unwrap(),
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x2000], 128).unwrap(),
+ ],
+ range.to_cidrs().as_slice()
+ );
+
+ let range = AddressRange::new_v6(
+ [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1001],
+ [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x2000],
+ )
+ .unwrap();
+
+ assert_eq!(
+ [
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1001], 128).unwrap(),
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1002], 127).unwrap(),
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1004], 126).unwrap(),
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1008], 125).unwrap(),
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1010], 124).unwrap(),
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1020], 123).unwrap(),
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1040], 122).unwrap(),
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1080], 121).unwrap(),
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1100], 120).unwrap(),
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1200], 119).unwrap(),
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1400], 118).unwrap(),
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1800], 117).unwrap(),
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x2000], 128).unwrap(),
+ ],
+ range.to_cidrs().as_slice()
+ );
+
+ let range = AddressRange::new_v6(
+ [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1001],
+ [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1001],
+ )
+ .unwrap();
+
+ assert_eq!(
+ [Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1001], 128).unwrap(),],
+ range.to_cidrs().as_slice()
+ );
+
+ let range = AddressRange::new_v6(
+ [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1001],
+ [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x2001],
+ )
+ .unwrap();
+
+ assert_eq!(
+ [
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1001], 128).unwrap(),
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1002], 127).unwrap(),
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1004], 126).unwrap(),
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1008], 125).unwrap(),
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1010], 124).unwrap(),
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1020], 123).unwrap(),
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1040], 122).unwrap(),
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1080], 121).unwrap(),
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1100], 120).unwrap(),
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1200], 119).unwrap(),
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1400], 118).unwrap(),
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1800], 117).unwrap(),
+ Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x2000], 127).unwrap(),
+ ],
+ range.to_cidrs().as_slice()
+ );
+
+ let range = AddressRange::new_v6(
+ [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0],
+ [0x2001, 0x0DB8, 0, 0, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF],
+ )
+ .unwrap();
+
+ assert_eq!(
+ [Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0], 64).unwrap()],
+ range.to_cidrs().as_slice()
+ );
+
+ let range = AddressRange::new_v6(
+ [0, 0, 0, 0, 0, 0, 0, 0],
+ [
+ 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
+ ],
+ )
+ .unwrap();
+
+ assert_eq!(
+ [Ipv6Cidr::new([0, 0, 0, 0, 0, 0, 0, 0], 0).unwrap(),],
+ range.to_cidrs().as_slice()
+ );
+
+ let range = AddressRange::new_v6(
+ [0, 0, 0, 0, 0, 0, 0, 0x0001],
+ [
+ 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
+ ],
+ )
+ .unwrap();
+
+ assert_eq!(
+ [
+ "::1/128".parse::<Ipv6Cidr>().unwrap(),
+ "::2/127".parse::<Ipv6Cidr>().unwrap(),
+ "::4/126".parse::<Ipv6Cidr>().unwrap(),
+ "::8/125".parse::<Ipv6Cidr>().unwrap(),
+ "::10/124".parse::<Ipv6Cidr>().unwrap(),
+ "::20/123".parse::<Ipv6Cidr>().unwrap(),
+ "::40/122".parse::<Ipv6Cidr>().unwrap(),
+ "::80/121".parse::<Ipv6Cidr>().unwrap(),
+ "::100/120".parse::<Ipv6Cidr>().unwrap(),
+ "::200/119".parse::<Ipv6Cidr>().unwrap(),
+ "::400/118".parse::<Ipv6Cidr>().unwrap(),
+ "::800/117".parse::<Ipv6Cidr>().unwrap(),
+ "::1000/116".parse::<Ipv6Cidr>().unwrap(),
+ "::2000/115".parse::<Ipv6Cidr>().unwrap(),
+ "::4000/114".parse::<Ipv6Cidr>().unwrap(),
+ "::8000/113".parse::<Ipv6Cidr>().unwrap(),
+ "::1:0/112".parse::<Ipv6Cidr>().unwrap(),
+ "::2:0/111".parse::<Ipv6Cidr>().unwrap(),
+ "::4:0/110".parse::<Ipv6Cidr>().unwrap(),
+ "::8:0/109".parse::<Ipv6Cidr>().unwrap(),
+ "::10:0/108".parse::<Ipv6Cidr>().unwrap(),
+ "::20:0/107".parse::<Ipv6Cidr>().unwrap(),
+ "::40:0/106".parse::<Ipv6Cidr>().unwrap(),
+ "::80:0/105".parse::<Ipv6Cidr>().unwrap(),
+ "::100:0/104".parse::<Ipv6Cidr>().unwrap(),
+ "::200:0/103".parse::<Ipv6Cidr>().unwrap(),
+ "::400:0/102".parse::<Ipv6Cidr>().unwrap(),
+ "::800:0/101".parse::<Ipv6Cidr>().unwrap(),
+ "::1000:0/100".parse::<Ipv6Cidr>().unwrap(),
+ "::2000:0/99".parse::<Ipv6Cidr>().unwrap(),
+ "::4000:0/98".parse::<Ipv6Cidr>().unwrap(),
+ "::8000:0/97".parse::<Ipv6Cidr>().unwrap(),
+ "::1:0:0/96".parse::<Ipv6Cidr>().unwrap(),
+ "::2:0:0/95".parse::<Ipv6Cidr>().unwrap(),
+ "::4:0:0/94".parse::<Ipv6Cidr>().unwrap(),
+ "::8:0:0/93".parse::<Ipv6Cidr>().unwrap(),
+ "::10:0:0/92".parse::<Ipv6Cidr>().unwrap(),
+ "::20:0:0/91".parse::<Ipv6Cidr>().unwrap(),
+ "::40:0:0/90".parse::<Ipv6Cidr>().unwrap(),
+ "::80:0:0/89".parse::<Ipv6Cidr>().unwrap(),
+ "::100:0:0/88".parse::<Ipv6Cidr>().unwrap(),
+ "::200:0:0/87".parse::<Ipv6Cidr>().unwrap(),
+ "::400:0:0/86".parse::<Ipv6Cidr>().unwrap(),
+ "::800:0:0/85".parse::<Ipv6Cidr>().unwrap(),
+ "::1000:0:0/84".parse::<Ipv6Cidr>().unwrap(),
+ "::2000:0:0/83".parse::<Ipv6Cidr>().unwrap(),
+ "::4000:0:0/82".parse::<Ipv6Cidr>().unwrap(),
+ "::8000:0:0/81".parse::<Ipv6Cidr>().unwrap(),
+ "::1:0:0:0/80".parse::<Ipv6Cidr>().unwrap(),
+ "::2:0:0:0/79".parse::<Ipv6Cidr>().unwrap(),
+ "::4:0:0:0/78".parse::<Ipv6Cidr>().unwrap(),
+ "::8:0:0:0/77".parse::<Ipv6Cidr>().unwrap(),
+ "::10:0:0:0/76".parse::<Ipv6Cidr>().unwrap(),
+ "::20:0:0:0/75".parse::<Ipv6Cidr>().unwrap(),
+ "::40:0:0:0/74".parse::<Ipv6Cidr>().unwrap(),
+ "::80:0:0:0/73".parse::<Ipv6Cidr>().unwrap(),
+ "::100:0:0:0/72".parse::<Ipv6Cidr>().unwrap(),
+ "::200:0:0:0/71".parse::<Ipv6Cidr>().unwrap(),
+ "::400:0:0:0/70".parse::<Ipv6Cidr>().unwrap(),
+ "::800:0:0:0/69".parse::<Ipv6Cidr>().unwrap(),
+ "::1000:0:0:0/68".parse::<Ipv6Cidr>().unwrap(),
+ "::2000:0:0:0/67".parse::<Ipv6Cidr>().unwrap(),
+ "::4000:0:0:0/66".parse::<Ipv6Cidr>().unwrap(),
+ "::8000:0:0:0/65".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:0:1::/64".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:0:2::/63".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:0:4::/62".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:0:8::/61".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:0:10::/60".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:0:20::/59".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:0:40::/58".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:0:80::/57".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:0:100::/56".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:0:200::/55".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:0:400::/54".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:0:800::/53".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:0:1000::/52".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:0:2000::/51".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:0:4000::/50".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:0:8000::/49".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:1::/48".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:2::/47".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:4::/46".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:8::/45".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:10::/44".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:20::/43".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:40::/42".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:80::/41".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:100::/40".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:200::/39".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:400::/38".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:800::/37".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:1000::/36".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:2000::/35".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:4000::/34".parse::<Ipv6Cidr>().unwrap(),
+ "0:0:8000::/33".parse::<Ipv6Cidr>().unwrap(),
+ "0:1::/32".parse::<Ipv6Cidr>().unwrap(),
+ "0:2::/31".parse::<Ipv6Cidr>().unwrap(),
+ "0:4::/30".parse::<Ipv6Cidr>().unwrap(),
+ "0:8::/29".parse::<Ipv6Cidr>().unwrap(),
+ "0:10::/28".parse::<Ipv6Cidr>().unwrap(),
+ "0:20::/27".parse::<Ipv6Cidr>().unwrap(),
+ "0:40::/26".parse::<Ipv6Cidr>().unwrap(),
+ "0:80::/25".parse::<Ipv6Cidr>().unwrap(),
+ "0:100::/24".parse::<Ipv6Cidr>().unwrap(),
+ "0:200::/23".parse::<Ipv6Cidr>().unwrap(),
+ "0:400::/22".parse::<Ipv6Cidr>().unwrap(),
+ "0:800::/21".parse::<Ipv6Cidr>().unwrap(),
+ "0:1000::/20".parse::<Ipv6Cidr>().unwrap(),
+ "0:2000::/19".parse::<Ipv6Cidr>().unwrap(),
+ "0:4000::/18".parse::<Ipv6Cidr>().unwrap(),
+ "0:8000::/17".parse::<Ipv6Cidr>().unwrap(),
+ "1::/16".parse::<Ipv6Cidr>().unwrap(),
+ "2::/15".parse::<Ipv6Cidr>().unwrap(),
+ "4::/14".parse::<Ipv6Cidr>().unwrap(),
+ "8::/13".parse::<Ipv6Cidr>().unwrap(),
+ "10::/12".parse::<Ipv6Cidr>().unwrap(),
+ "20::/11".parse::<Ipv6Cidr>().unwrap(),
+ "40::/10".parse::<Ipv6Cidr>().unwrap(),
+ "80::/9".parse::<Ipv6Cidr>().unwrap(),
+ "100::/8".parse::<Ipv6Cidr>().unwrap(),
+ "200::/7".parse::<Ipv6Cidr>().unwrap(),
+ "400::/6".parse::<Ipv6Cidr>().unwrap(),
+ "800::/5".parse::<Ipv6Cidr>().unwrap(),
+ "1000::/4".parse::<Ipv6Cidr>().unwrap(),
+ "2000::/3".parse::<Ipv6Cidr>().unwrap(),
+ "4000::/2".parse::<Ipv6Cidr>().unwrap(),
+ "8000::/1".parse::<Ipv6Cidr>().unwrap(),
+ ],
+ range.to_cidrs().as_slice()
+ );
+
+ let range = AddressRange::new_v6(
+ [0, 0, 0, 0, 0, 0, 0, 0],
+ [
+ 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFE,
+ ],
+ )
+ .unwrap();
+
+ assert_eq!(
+ [
+ "::/1".parse::<Ipv6Cidr>().unwrap(),
+ "8000::/2".parse::<Ipv6Cidr>().unwrap(),
+ "c000::/3".parse::<Ipv6Cidr>().unwrap(),
+ "e000::/4".parse::<Ipv6Cidr>().unwrap(),
+ "f000::/5".parse::<Ipv6Cidr>().unwrap(),
+ "f800::/6".parse::<Ipv6Cidr>().unwrap(),
+ "fc00::/7".parse::<Ipv6Cidr>().unwrap(),
+ "fe00::/8".parse::<Ipv6Cidr>().unwrap(),
+ "ff00::/9".parse::<Ipv6Cidr>().unwrap(),
+ "ff80::/10".parse::<Ipv6Cidr>().unwrap(),
+ "ffc0::/11".parse::<Ipv6Cidr>().unwrap(),
+ "ffe0::/12".parse::<Ipv6Cidr>().unwrap(),
+ "fff0::/13".parse::<Ipv6Cidr>().unwrap(),
+ "fff8::/14".parse::<Ipv6Cidr>().unwrap(),
+ "fffc::/15".parse::<Ipv6Cidr>().unwrap(),
+ "fffe::/16".parse::<Ipv6Cidr>().unwrap(),
+ "ffff::/17".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:8000::/18".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:c000::/19".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:e000::/20".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:f000::/21".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:f800::/22".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:fc00::/23".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:fe00::/24".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ff00::/25".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ff80::/26".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffc0::/27".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffe0::/28".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:fff0::/29".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:fff8::/30".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:fffc::/31".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:fffe::/32".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff::/33".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:8000::/34".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:c000::/35".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:e000::/36".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:f000::/37".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:f800::/38".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:fc00::/39".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:fe00::/40".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ff00::/41".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ff80::/42".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffc0::/43".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffe0::/44".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:fff0::/45".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:fff8::/46".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:fffc::/47".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:fffe::/48".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff::/49".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:8000::/50".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:c000::/51".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:e000::/52".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:f000::/53".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:f800::/54".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:fc00::/55".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:fe00::/56".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:ff00::/57".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:ff80::/58".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:ffc0::/59".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:ffe0::/60".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:fff0::/61".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:fff8::/62".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:fffc::/63".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:fffe::/64".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:ffff::/65".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:ffff:8000::/66".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:ffff:c000::/67".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:ffff:e000::/68".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:ffff:f000::/69".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:ffff:f800::/70".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:ffff:fc00::/71".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:ffff:fe00::/72".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:ffff:ff00::/73".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:ffff:ff80::/74".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:ffff:ffc0::/75".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:ffff:ffe0::/76".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:ffff:fff0::/77".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:ffff:fff8::/78".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:ffff:fffc::/79".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:ffff:fffe::/80".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:ffff:ffff::/81".parse::<Ipv6Cidr>().unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:8000::/82"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:c000::/83"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:e000::/84"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:f000::/85"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:f800::/86"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:fc00::/87"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:fe00::/88"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ff00::/89"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ff80::/90"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffc0::/91"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffe0::/92"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:fff0::/93"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:fff8::/94"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:fffc::/95"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:fffe::/96"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff::/97"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:8000:0/98"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:c000:0/99"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:e000:0/100"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:f000:0/101"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:f800:0/102"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:fc00:0/103"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:fe00:0/104"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:ff00:0/105"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:ff80:0/106"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:ffc0:0/107"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:ffe0:0/108"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:fff0:0/109"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:fff8:0/110"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:fffc:0/111"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:fffe:0/112"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:ffff:0/113"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:ffff:8000/114"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:ffff:c000/115"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:ffff:e000/116"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:ffff:f000/117"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:ffff:f800/118"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fc00/119"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fe00/120"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00/121"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff80/122"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffc0/123"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffe0/124"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fff0/125"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fff8/126"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffc/127"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe/128"
+ .parse::<Ipv6Cidr>()
+ .unwrap(),
+ ],
+ range.to_cidrs().as_slice()
+ );
+
+ let range =
+ AddressRange::new_v6([0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0]).unwrap();
+
+ assert_eq!(
+ [Ipv6Cidr::new([0, 0, 0, 0, 0, 0, 0, 0], 128).unwrap(),],
+ range.to_cidrs().as_slice()
+ );
+
+ let range = AddressRange::new_v6(
+ [
+ 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
+ ],
+ [
+ 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
+ ],
+ )
+ .unwrap();
+
+ assert_eq!(
+ [Ipv6Cidr::new(
+ [0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF],
+ 128
+ )
+ .unwrap(),],
+ range.to_cidrs().as_slice()
+ );
+ }
+}
diff --git a/proxmox-network-types/src/lib.rs b/proxmox-network-types/src/lib.rs
new file mode 100644
index 00000000..ee26b1c1
--- /dev/null
+++ b/proxmox-network-types/src/lib.rs
@@ -0,0 +1,5 @@
+#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
+#![deny(unsafe_op_in_unsafe_fn)]
+
+pub mod ip_address;
+pub mod mac_address;
diff --git a/proxmox-network-types/src/mac_address.rs b/proxmox-network-types/src/mac_address.rs
new file mode 100644
index 00000000..d347076e
--- /dev/null
+++ b/proxmox-network-types/src/mac_address.rs
@@ -0,0 +1,121 @@
+use std::fmt::Display;
+use std::net::Ipv6Addr;
+
+use serde_with::{DeserializeFromStr, SerializeDisplay};
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum MacAddressError {
+ #[error("the hostname must be from 1 to 63 characters long")]
+ InvalidLength,
+ #[error("the hostname contains invalid symbols")]
+ InvalidSymbols,
+}
+
+/// EUI-48 MAC Address
+#[derive(
+ Clone, Copy, Debug, DeserializeFromStr, SerializeDisplay, PartialEq, Eq, Hash, PartialOrd, Ord,
+)]
+pub struct MacAddress([u8; 6]);
+
+static LOCAL_PART: [u8; 8] = [0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
+static EUI64_MIDDLE_PART: [u8; 2] = [0xFF, 0xFE];
+
+impl MacAddress {
+ pub fn new(address: [u8; 6]) -> Self {
+ Self(address)
+ }
+
+ /// generates a link local IPv6-address according to RFC 4291 (Appendix A)
+ pub fn eui64_link_local_address(&self) -> Ipv6Addr {
+ let head = &self.0[..3];
+ let tail = &self.0[3..];
+
+ let mut eui64_address: Vec<u8> = LOCAL_PART
+ .iter()
+ .chain(head.iter())
+ .chain(EUI64_MIDDLE_PART.iter())
+ .chain(tail.iter())
+ .copied()
+ .collect();
+
+ // we need to flip the 7th bit of the first eui64 byte
+ eui64_address[8] ^= 0x02;
+
+ Ipv6Addr::from(
+ TryInto::<[u8; 16]>::try_into(eui64_address).expect("is an u8 array with 16 entries"),
+ )
+ }
+}
+
+impl std::str::FromStr for MacAddress {
+ type Err = MacAddressError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let split = s.split(':');
+
+ let parsed = split
+ .into_iter()
+ .map(|elem| u8::from_str_radix(elem, 16))
+ .collect::<Result<Vec<u8>, _>>()
+ .map_err(|_| MacAddressError::InvalidSymbols)?;
+
+ if parsed.len() != 6 {
+ return Err(MacAddressError::InvalidLength);
+ }
+
+ // SAFETY: ok because of length check
+ Ok(Self(parsed.try_into().unwrap()))
+ }
+}
+
+impl Display for MacAddress {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "{:<02X}:{:<02X}:{:<02X}:{:<02X}:{:<02X}:{:<02X}",
+ self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5]
+ )
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::str::FromStr;
+
+ #[test]
+ fn test_parse_mac_address() {
+ for input in [
+ "aa:aa:aa:11:22:33",
+ "AA:BB:FF:11:22:33",
+ "bc:24:11:AA:bb:Ef",
+ ] {
+ let mac_address = input.parse::<MacAddress>().expect("valid mac address");
+
+ assert_eq!(input.to_uppercase(), mac_address.to_string());
+ }
+
+ for input in [
+ "aa:aa:aa:11:22:33:aa",
+ "AA:BB:FF:11:22",
+ "AA:BB:GG:11:22:33",
+ "AABBGG112233",
+ "",
+ ] {
+ input
+ .parse::<MacAddress>()
+ .expect_err("invalid mac address");
+ }
+ }
+
+ #[test]
+ fn test_eui64_link_local_address() {
+ let mac_address: MacAddress = "BC:24:11:49:8D:75".parse().expect("valid MAC address");
+
+ let link_local_address =
+ Ipv6Addr::from_str("fe80::be24:11ff:fe49:8d75").expect("valid IPv6 address");
+
+ assert_eq!(link_local_address, mac_address.eui64_link_local_address());
+ }
+}
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox v3 2/4] network-types: make cidr and mac-address types usable by the api
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox v3 1/4] network-types: initial commit Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox v3 3/4] network-types: add api types for ipv4/6 Stefan Hanreich
` (72 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
Implement ApiType and UpdaterType in order to be able to directly use
the CIDR and MacAddress types in the API. Their schema is a string and
they get (de-)serialized by the respective FromStr / Display
implementations.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-network-types/Cargo.toml | 1 +
proxmox-network-types/src/ip_address.rs | 36 ++++++++++++++++++++++++
proxmox-network-types/src/mac_address.rs | 25 ++++++++++++++++
3 files changed, 62 insertions(+)
diff --git a/proxmox-network-types/Cargo.toml b/proxmox-network-types/Cargo.toml
index aa6d66bc..3dda51d4 100644
--- a/proxmox-network-types/Cargo.toml
+++ b/proxmox-network-types/Cargo.toml
@@ -19,3 +19,4 @@ proxmox-schema = { workspace = true, features = [ "api-macro", "api-types" ], op
[features]
default = []
+api-types = ["dep:proxmox-schema", "dep:regex"]
diff --git a/proxmox-network-types/src/ip_address.rs b/proxmox-network-types/src/ip_address.rs
index 355547b1..827141b9 100644
--- a/proxmox-network-types/src/ip_address.rs
+++ b/proxmox-network-types/src/ip_address.rs
@@ -5,6 +5,12 @@ use std::net::{AddrParseError, IpAddr, Ipv4Addr, Ipv6Addr};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use thiserror::Error;
+#[cfg(feature = "api-types")]
+use proxmox_schema::{ApiType, Schema, UpdaterType};
+
+#[cfg(feature = "api-types")]
+use proxmox_schema::api_types::{CIDR_SCHEMA, CIDR_V4_SCHEMA, CIDR_V6_SCHEMA};
+
/// The family (v4 or v6) of an IP address or CIDR prefix
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Family {
@@ -48,6 +54,16 @@ pub enum Cidr {
Ipv6(Ipv6Cidr),
}
+#[cfg(feature = "api-types")]
+impl ApiType for Cidr {
+ const API_SCHEMA: Schema = CIDR_SCHEMA;
+}
+
+#[cfg(feature = "api-types")]
+impl UpdaterType for Cidr {
+ type Updater = Option<Cidr>;
+}
+
impl Cidr {
pub fn new_v4(addr: impl Into<Ipv4Addr>, mask: u8) -> Result<Self, CidrError> {
Ok(Cidr::Ipv4(Ipv4Cidr::new(addr, mask)?))
@@ -139,6 +155,16 @@ pub struct Ipv4Cidr {
mask: u8,
}
+#[cfg(feature = "api-types")]
+impl ApiType for Ipv4Cidr {
+ const API_SCHEMA: Schema = CIDR_V4_SCHEMA;
+}
+
+#[cfg(feature = "api-types")]
+impl UpdaterType for Ipv4Cidr {
+ type Updater = Option<Ipv4Cidr>;
+}
+
impl Ipv4Cidr {
pub fn new(addr: impl Into<Ipv4Addr>, mask: u8) -> Result<Self, CidrError> {
if mask > IPV4_LENGTH {
@@ -217,6 +243,16 @@ pub struct Ipv6Cidr {
mask: u8,
}
+#[cfg(feature = "api-types")]
+impl ApiType for Ipv6Cidr {
+ const API_SCHEMA: Schema = CIDR_V6_SCHEMA;
+}
+
+#[cfg(feature = "api-types")]
+impl UpdaterType for Ipv6Cidr {
+ type Updater = Option<Ipv6Cidr>;
+}
+
impl Ipv6Cidr {
pub fn new(addr: impl Into<Ipv6Addr>, mask: u8) -> Result<Self, CidrError> {
if mask > IPV6_LENGTH {
diff --git a/proxmox-network-types/src/mac_address.rs b/proxmox-network-types/src/mac_address.rs
index d347076e..4ad82699 100644
--- a/proxmox-network-types/src/mac_address.rs
+++ b/proxmox-network-types/src/mac_address.rs
@@ -4,6 +4,9 @@ use std::net::Ipv6Addr;
use serde_with::{DeserializeFromStr, SerializeDisplay};
use thiserror::Error;
+#[cfg(feature = "api-types")]
+use proxmox_schema::{const_regex, ApiStringFormat, ApiType, Schema, StringSchema, UpdaterType};
+
#[derive(Error, Debug)]
pub enum MacAddressError {
#[error("the hostname must be from 1 to 63 characters long")]
@@ -12,12 +15,34 @@ pub enum MacAddressError {
InvalidSymbols,
}
+#[cfg(feature = "api-types")]
+const_regex! {
+ pub MAC_ADDRESS_REGEX = r"([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}";
+}
+
+#[cfg(feature = "api-types")]
+pub const MAC_ADDRESS_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&MAC_ADDRESS_REGEX);
+
/// EUI-48 MAC Address
#[derive(
Clone, Copy, Debug, DeserializeFromStr, SerializeDisplay, PartialEq, Eq, Hash, PartialOrd, Ord,
)]
pub struct MacAddress([u8; 6]);
+#[cfg(feature = "api-types")]
+impl ApiType for MacAddress {
+ const API_SCHEMA: Schema = StringSchema::new("MAC address")
+ .min_length(17)
+ .max_length(17)
+ .format(&MAC_ADDRESS_FORMAT)
+ .schema();
+}
+
+#[cfg(feature = "api-types")]
+impl UpdaterType for MacAddress {
+ type Updater = Option<MacAddress>;
+}
+
static LOCAL_PART: [u8; 8] = [0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
static EUI64_MIDDLE_PART: [u8; 2] = [0xFF, 0xFE];
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox v3 3/4] network-types: add api types for ipv4/6
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox v3 1/4] network-types: initial commit Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox v3 2/4] network-types: make cidr and mac-address types usable by the api Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox v3 4/4] api-macro: add allof schema to enum Stefan Hanreich
` (71 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
Add two types that transparently wrap the std structs for IPv4 and
IPv6 addresses as well as MacAddress, so they can be used directly in
structs with the API macro. Similar to their CIDR counterparts, they
have a StringSchema and are (de-)serialized via the respective
FromStr and Display implementations.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-network-types/src/ip_address.rs | 126 ++++++++++++++++++++++++
1 file changed, 126 insertions(+)
diff --git a/proxmox-network-types/src/ip_address.rs b/proxmox-network-types/src/ip_address.rs
index 827141b9..8c21453a 100644
--- a/proxmox-network-types/src/ip_address.rs
+++ b/proxmox-network-types/src/ip_address.rs
@@ -11,6 +11,132 @@ use proxmox_schema::{ApiType, Schema, UpdaterType};
#[cfg(feature = "api-types")]
use proxmox_schema::api_types::{CIDR_SCHEMA, CIDR_V4_SCHEMA, CIDR_V6_SCHEMA};
+#[cfg(feature = "api-types")]
+pub mod api_types {
+ use std::net::AddrParseError;
+ use std::ops::{Deref, DerefMut};
+
+ use proxmox_schema::api_types::IP_V6_SCHEMA;
+ use proxmox_schema::{api_types::IP_V4_SCHEMA, ApiType, UpdaterType};
+ use serde_with::{DeserializeFromStr, SerializeDisplay};
+
+ #[derive(
+ Debug,
+ Clone,
+ Copy,
+ Eq,
+ PartialEq,
+ Ord,
+ PartialOrd,
+ DeserializeFromStr,
+ SerializeDisplay,
+ Hash,
+ )]
+ #[repr(transparent)]
+ pub struct Ipv4Addr(std::net::Ipv4Addr);
+
+ impl ApiType for Ipv4Addr {
+ const API_SCHEMA: proxmox_schema::Schema = IP_V4_SCHEMA;
+ }
+
+ impl UpdaterType for Ipv4Addr {
+ type Updater = Option<Ipv4Addr>;
+ }
+
+ impl Deref for Ipv4Addr {
+ type Target = std::net::Ipv4Addr;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+ }
+
+ impl DerefMut for Ipv4Addr {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+ }
+
+ impl std::str::FromStr for Ipv4Addr {
+ type Err = AddrParseError;
+
+ fn from_str(value: &str) -> Result<Self, Self::Err> {
+ let ip_address = std::net::Ipv4Addr::from_str(value)?;
+ Ok(Self(ip_address))
+ }
+ }
+
+ impl std::fmt::Display for Ipv4Addr {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+ }
+
+ impl From<std::net::Ipv4Addr> for Ipv4Addr {
+ fn from(value: std::net::Ipv4Addr) -> Self {
+ Self(value)
+ }
+ }
+
+ #[derive(
+ Debug,
+ Clone,
+ Copy,
+ Eq,
+ PartialEq,
+ Ord,
+ PartialOrd,
+ DeserializeFromStr,
+ SerializeDisplay,
+ Hash,
+ )]
+ #[repr(transparent)]
+ pub struct Ipv6Addr(std::net::Ipv6Addr);
+
+ impl ApiType for Ipv6Addr {
+ const API_SCHEMA: proxmox_schema::Schema = IP_V6_SCHEMA;
+ }
+
+ impl UpdaterType for Ipv6Addr {
+ type Updater = Option<Ipv6Addr>;
+ }
+
+ impl Deref for Ipv6Addr {
+ type Target = std::net::Ipv6Addr;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+ }
+
+ impl DerefMut for Ipv6Addr {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+ }
+
+ impl std::str::FromStr for Ipv6Addr {
+ type Err = AddrParseError;
+
+ fn from_str(value: &str) -> Result<Self, Self::Err> {
+ let ip_address = std::net::Ipv6Addr::from_str(value)?;
+ Ok(Self(ip_address))
+ }
+ }
+
+ impl std::fmt::Display for Ipv6Addr {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+ }
+
+ impl From<std::net::Ipv6Addr> for Ipv6Addr {
+ fn from(value: std::net::Ipv6Addr) -> Self {
+ Self(value)
+ }
+ }
+}
+
/// The family (v4 or v6) of an IP address or CIDR prefix
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Family {
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox v3 4/4] api-macro: add allof schema to enum
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (2 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox v3 3/4] network-types: add api types for ipv4/6 Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-firewall v3 1/1] firewall: nftables: migrate to proxmox-network-types Stefan Hanreich
` (70 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
The API macro required the enum variants to either have a oneOf or
ObjectSchema, but did not allow allOf schemas. There's not really a
reason to not allow allOf as well, since they implement
ObjectSchemaType as well and represent an ObjectSchema, just like
oneOf and ObjectSchema do.
This is in preparation for the SDN fabrics, where sections use the
allOf schema to merge general properties with protocol-specific
properties.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-api-macro/src/api/enums.rs | 1 +
1 file changed, 1 insertion(+)
diff --git a/proxmox-api-macro/src/api/enums.rs b/proxmox-api-macro/src/api/enums.rs
index 9b122f9c..31a715db 100644
--- a/proxmox-api-macro/src/api/enums.rs
+++ b/proxmox-api-macro/src/api/enums.rs
@@ -253,6 +253,7 @@ fn handle_section_config_enum(
match &<#ty as ::proxmox_schema::ApiType>::API_SCHEMA {
::proxmox_schema::Schema::Object(schema) => schema,
::proxmox_schema::Schema::OneOf(schema) => schema,
+ ::proxmox_schema::Schema::AllOf(schema) => schema,
_ => panic!("enum requires an object schema"),
}
}
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-firewall v3 1/1] firewall: nftables: migrate to proxmox-network-types
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (3 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox v3 4/4] api-macro: add allof schema to enum Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 01/21] config: use proxmox_serde perl helpers Stefan Hanreich
` (69 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
The fabrics patch series moved some generic network types into its own
crate, so they can be reused across crates. Migrate proxmox-firewall
to use the new proxmox-network-types crate instead of
proxmox_ve_config. No functional changes intended.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
Cargo.toml | 1 +
proxmox-firewall/Cargo.toml | 1 +
proxmox-firewall/src/firewall.rs | 8 ++++----
proxmox-firewall/src/object.rs | 4 +++-
proxmox-firewall/src/rule.rs | 3 ++-
proxmox-nftables/Cargo.toml | 3 ++-
proxmox-nftables/src/expression.rs | 7 +++----
proxmox-nftables/src/types.rs | 2 +-
8 files changed, 17 insertions(+), 12 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index cdb4a53..97138ae 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,3 +7,4 @@ resolver = "2"
[workspace.dependencies]
proxmox-ve-config = { version = "0.2.3" }
+proxmox-network-types = { version = "0.1" }
diff --git a/proxmox-firewall/Cargo.toml b/proxmox-firewall/Cargo.toml
index a9abf93..167608d 100644
--- a/proxmox-firewall/Cargo.toml
+++ b/proxmox-firewall/Cargo.toml
@@ -22,6 +22,7 @@ signal-hook = "0.3"
proxmox-nftables = { path = "../proxmox-nftables", features = ["config-ext"] }
proxmox-ve-config = { workspace = true }
+proxmox-network-types = { workspace = true }
[dev-dependencies]
insta = { version = "1.21", features = ["json"] }
diff --git a/proxmox-firewall/src/firewall.rs b/proxmox-firewall/src/firewall.rs
index 086b96c..635cf42 100644
--- a/proxmox-firewall/src/firewall.rs
+++ b/proxmox-firewall/src/firewall.rs
@@ -20,7 +20,7 @@ use proxmox_ve_config::firewall::ct_helper::get_cthelper;
use proxmox_ve_config::firewall::guest::Config as GuestConfig;
use proxmox_ve_config::firewall::host::Config as HostConfig;
-use proxmox_ve_config::firewall::types::address::Ipv6Cidr;
+use proxmox_network_types::ip_address::{Cidr, Ipv6Cidr};
use proxmox_ve_config::firewall::types::ipset::{
Ipfilter, Ipset, IpsetEntry, IpsetName, IpsetScope,
};
@@ -808,14 +808,14 @@ impl Firewall {
let cidr =
Ipv6Cidr::from(network_device.mac_address().eui64_link_local_address());
- ipset.push(cidr.into());
+ ipset.push(IpsetEntry::from(Cidr::from(cidr)));
if let Some(ip_address) = network_device.ip() {
- ipset.push(IpsetEntry::from(ip_address));
+ ipset.push(IpsetEntry::from(Cidr::from(ip_address)));
}
if let Some(ip6_address) = network_device.ip6() {
- ipset.push(IpsetEntry::from(ip6_address));
+ ipset.push(IpsetEntry::from(Cidr::from(ip6_address)));
}
commands.append(&mut ipset.to_nft_objects(&env)?);
diff --git a/proxmox-firewall/src/object.rs b/proxmox-firewall/src/object.rs
index cf7e773..cbfadba 100644
--- a/proxmox-firewall/src/object.rs
+++ b/proxmox-firewall/src/object.rs
@@ -11,11 +11,13 @@ use proxmox_nftables::{
use proxmox_ve_config::{
firewall::{
ct_helper::CtHelperMacro,
- types::{address::Family, alias::AliasName, ipset::IpsetAddress, Alias, Ipset},
+ types::{alias::AliasName, ipset::IpsetAddress, Alias, Ipset},
},
guest::types::Vmid,
};
+use proxmox_network_types::ip_address::Family;
+
use crate::config::FirewallConfig;
pub(crate) struct NftObjectEnv<'a, 'b> {
diff --git a/proxmox-firewall/src/rule.rs b/proxmox-firewall/src/rule.rs
index 14ee544..16a0b5a 100644
--- a/proxmox-firewall/src/rule.rs
+++ b/proxmox-firewall/src/rule.rs
@@ -12,7 +12,6 @@ use proxmox_ve_config::{
ct_helper::CtHelperMacro,
fw_macros::{get_macro, FwMacro},
types::{
- address::Family,
alias::AliasName,
ipset::{Ipfilter, IpsetName},
log::LogRateLimit,
@@ -26,6 +25,8 @@ use proxmox_ve_config::{
guest::types::Vmid,
};
+use proxmox_network_types::ip_address::Family;
+
use crate::config::FirewallConfig;
#[derive(Debug, Clone)]
diff --git a/proxmox-nftables/Cargo.toml b/proxmox-nftables/Cargo.toml
index 4ff6f41..85f07f0 100644
--- a/proxmox-nftables/Cargo.toml
+++ b/proxmox-nftables/Cargo.toml
@@ -11,7 +11,7 @@ description = "Proxmox VE nftables"
license = "AGPL-3"
[features]
-config-ext = ["dep:proxmox-ve-config"]
+config-ext = ["dep:proxmox-ve-config", "dep:proxmox-network-types"]
[dependencies]
log = "0.4"
@@ -23,3 +23,4 @@ serde_json = "1"
serde_plain = "1"
proxmox-ve-config = { workspace = true, optional = true }
+proxmox-network-types = { workspace = true, optional = true }
diff --git a/proxmox-nftables/src/expression.rs b/proxmox-nftables/src/expression.rs
index e9ef94f..bac0763 100644
--- a/proxmox-nftables/src/expression.rs
+++ b/proxmox-nftables/src/expression.rs
@@ -1,17 +1,16 @@
use crate::types::{ElemConfig, Verdict};
-use proxmox_ve_config::firewall::types::address::IpRange;
use proxmox_ve_config::host::types::BridgeName;
use serde::{Deserialize, Serialize};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
#[cfg(feature = "config-ext")]
-use proxmox_ve_config::firewall::types::address::{Family, IpEntry, IpList};
+use proxmox_network_types::ip_address::{Cidr, Family, IpRange};
+#[cfg(feature = "config-ext")]
+use proxmox_ve_config::firewall::types::address::{IpEntry, IpList};
#[cfg(feature = "config-ext")]
use proxmox_ve_config::firewall::types::port::{PortEntry, PortList};
#[cfg(feature = "config-ext")]
use proxmox_ve_config::firewall::types::rule_match::{IcmpCode, IcmpType, Icmpv6Code, Icmpv6Type};
-#[cfg(feature = "config-ext")]
-use proxmox_ve_config::firewall::types::Cidr;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
diff --git a/proxmox-nftables/src/types.rs b/proxmox-nftables/src/types.rs
index 320c757..c613e64 100644
--- a/proxmox-nftables/src/types.rs
+++ b/proxmox-nftables/src/types.rs
@@ -8,7 +8,7 @@ use crate::{Expression, Statement};
use serde::{Deserialize, Serialize};
#[cfg(feature = "config-ext")]
-use proxmox_ve_config::firewall::types::address::Family;
+use proxmox_network_types::ip_address::Family;
#[cfg(feature = "config-ext")]
use proxmox_ve_config::firewall::types::ipset::IpsetName;
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs v3 01/21] config: use proxmox_serde perl helpers
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (4 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-firewall v3 1/1] firewall: nftables: migrate to proxmox-network-types Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 02/21] ve-config: move types to proxmox-network-types Stefan Hanreich
` (68 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
proxmox_serde provides helpers for parsing optional numbers / booleans
coming from perl, so move to using them instead of implementing our
own versions here. No functional changes intended.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/Cargo.toml | 1 +
proxmox-ve-config/debian/control | 4 ++
proxmox-ve-config/src/firewall/bridge.rs | 3 +-
proxmox-ve-config/src/firewall/cluster.rs | 6 +-
proxmox-ve-config/src/firewall/guest.rs | 14 ++--
proxmox-ve-config/src/firewall/host.rs | 26 ++++----
proxmox-ve-config/src/firewall/parse.rs | 80 -----------------------
7 files changed, 28 insertions(+), 106 deletions(-)
diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml
index 6c7a47e..e998b45 100644
--- a/proxmox-ve-config/Cargo.toml
+++ b/proxmox-ve-config/Cargo.toml
@@ -16,6 +16,7 @@ serde = { version = "1", features = [ "derive" ] }
serde_json = "1"
serde_plain = "1"
serde_with = "3"
+proxmox-serde = { version = "0.1.2", features = [ "perl" ]}
proxmox-schema = "4"
proxmox-sys = "0.6.4"
diff --git a/proxmox-ve-config/debian/control b/proxmox-ve-config/debian/control
index f412943..2c43724 100644
--- a/proxmox-ve-config/debian/control
+++ b/proxmox-ve-config/debian/control
@@ -10,6 +10,8 @@ Build-Depends-Arch: cargo:native <!nocheck>,
librust-log-0.4+default-dev <!nocheck>,
librust-nix-0.26+default-dev <!nocheck>,
librust-proxmox-schema-4+default-dev <!nocheck>,
+ librust-proxmox-serde-0.1+default-dev (>= 0.1.2-~~) <!nocheck>,
+ librust-proxmox-serde-0.1+perl-dev (>= 0.1.2-~~) <!nocheck>,
librust-proxmox-sortable-macro-0.1+default-dev (>= 0.1.3-~~) <!nocheck>,
librust-proxmox-sys-0.6+default-dev (>= 0.6.4-~~) <!nocheck>,
librust-serde-1+default-dev <!nocheck>,
@@ -34,6 +36,8 @@ Depends:
librust-log-0.4+default-dev,
librust-nix-0.26+default-dev,
librust-proxmox-schema-4+default-dev,
+ librust-proxmox-serde-0.1+default-dev (>= 0.1.2-~~),
+ librust-proxmox-serde-0.1+perl-dev (>= 0.1.2-~~),
librust-proxmox-sortable-macro-0.1+default-dev (>= 0.1.3-~~),
librust-proxmox-sys-0.6+default-dev (>= 0.6.4-~~),
librust-serde-1+default-dev,
diff --git a/proxmox-ve-config/src/firewall/bridge.rs b/proxmox-ve-config/src/firewall/bridge.rs
index 4acb6fa..6dea60e 100644
--- a/proxmox-ve-config/src/firewall/bridge.rs
+++ b/proxmox-ve-config/src/firewall/bridge.rs
@@ -3,7 +3,6 @@ use std::io;
use anyhow::Error;
use serde::Deserialize;
-use crate::firewall::parse::serde_option_bool;
use crate::firewall::types::log::LogLevel;
use crate::firewall::types::rule::{Direction, Verdict};
@@ -55,7 +54,7 @@ impl Config {
#[derive(Debug, Default, Deserialize)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Options {
- #[serde(default, with = "serde_option_bool")]
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
enable: Option<bool>,
policy_forward: Option<Verdict>,
diff --git a/proxmox-ve-config/src/firewall/cluster.rs b/proxmox-ve-config/src/firewall/cluster.rs
index ce3dd53..a775cd9 100644
--- a/proxmox-ve-config/src/firewall/cluster.rs
+++ b/proxmox-ve-config/src/firewall/cluster.rs
@@ -10,7 +10,7 @@ use crate::firewall::types::log::LogRateLimit;
use crate::firewall::types::rule::{Direction, Verdict};
use crate::firewall::types::{Alias, Group, Rule};
-use crate::firewall::parse::{serde_option_bool, serde_option_log_ratelimit};
+use crate::firewall::parse::serde_option_log_ratelimit;
#[derive(Debug, Default)]
pub struct Config {
@@ -118,10 +118,10 @@ impl Config {
#[derive(Debug, Default, Deserialize)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Options {
- #[serde(default, with = "serde_option_bool")]
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
enable: Option<bool>,
- #[serde(default, with = "serde_option_bool")]
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
ebtables: Option<bool>,
#[serde(default, with = "serde_option_log_ratelimit")]
diff --git a/proxmox-ve-config/src/firewall/guest.rs b/proxmox-ve-config/src/firewall/guest.rs
index 23eaa4e..4428a75 100644
--- a/proxmox-ve-config/src/firewall/guest.rs
+++ b/proxmox-ve-config/src/firewall/guest.rs
@@ -13,8 +13,6 @@ use crate::firewall::types::Ipset;
use anyhow::{bail, Error};
use serde::Deserialize;
-use crate::firewall::parse::serde_option_bool;
-
/// default return value for [`Config::is_enabled()`]
pub const GUEST_ENABLED_DEFAULT: bool = false;
/// default return value for [`Config::allow_ndp()`]
@@ -37,25 +35,25 @@ pub const GUEST_POLICY_FORWARD_DEFAULT: Verdict = Verdict::Accept;
#[derive(Debug, Default, Deserialize)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Options {
- #[serde(default, with = "serde_option_bool")]
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
dhcp: Option<bool>,
- #[serde(default, with = "serde_option_bool")]
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
enable: Option<bool>,
- #[serde(default, with = "serde_option_bool")]
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
ipfilter: Option<bool>,
- #[serde(default, with = "serde_option_bool")]
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
ndp: Option<bool>,
- #[serde(default, with = "serde_option_bool")]
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
radv: Option<bool>,
log_level_in: Option<LogLevel>,
log_level_out: Option<LogLevel>,
- #[serde(default, with = "serde_option_bool")]
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
macfilter: Option<bool>,
#[serde(rename = "policy_in")]
diff --git a/proxmox-ve-config/src/firewall/host.rs b/proxmox-ve-config/src/firewall/host.rs
index 394896c..f7b02f9 100644
--- a/proxmox-ve-config/src/firewall/host.rs
+++ b/proxmox-ve-config/src/firewall/host.rs
@@ -36,49 +36,49 @@ pub const HOST_LOG_INVALID_CONNTRACK: bool = false;
#[derive(Debug, Default, Deserialize)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Options {
- #[serde(default, with = "parse::serde_option_bool")]
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
enable: Option<bool>,
- #[serde(default, with = "parse::serde_option_bool")]
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
nftables: Option<bool>,
log_level_in: Option<LogLevel>,
log_level_out: Option<LogLevel>,
log_level_forward: Option<LogLevel>,
- #[serde(default, with = "parse::serde_option_bool")]
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
log_nf_conntrack: Option<bool>,
- #[serde(default, with = "parse::serde_option_bool")]
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
ndp: Option<bool>,
- #[serde(default, with = "parse::serde_option_bool")]
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
nf_conntrack_allow_invalid: Option<bool>,
// is Option<Vec<>> for easier deserialization
#[serde(default, with = "parse::serde_option_conntrack_helpers")]
nf_conntrack_helpers: Option<Vec<String>>,
- #[serde(default, with = "parse::serde_option_number")]
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_i64")]
nf_conntrack_max: Option<i64>,
- #[serde(default, with = "parse::serde_option_number")]
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_i64")]
nf_conntrack_tcp_timeout_established: Option<i64>,
- #[serde(default, with = "parse::serde_option_number")]
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_i64")]
nf_conntrack_tcp_timeout_syn_recv: Option<i64>,
- #[serde(default, with = "parse::serde_option_bool")]
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
nosmurfs: Option<bool>,
- #[serde(default, with = "parse::serde_option_bool")]
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
protection_synflood: Option<bool>,
- #[serde(default, with = "parse::serde_option_number")]
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_i64")]
protection_synflood_burst: Option<i64>,
- #[serde(default, with = "parse::serde_option_number")]
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_i64")]
protection_synflood_rate: Option<i64>,
smurf_log_level: Option<LogLevel>,
tcp_flags_log_level: Option<LogLevel>,
- #[serde(default, with = "parse::serde_option_bool")]
+ #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
tcpflags: Option<bool>,
}
diff --git a/proxmox-ve-config/src/firewall/parse.rs b/proxmox-ve-config/src/firewall/parse.rs
index 8cf4757..7fd5c84 100644
--- a/proxmox-ve-config/src/firewall/parse.rs
+++ b/proxmox-ve-config/src/firewall/parse.rs
@@ -148,86 +148,6 @@ pub fn parse_named_section_tail<'a>(
})
}
-// parses a number from a string OR number
-pub mod serde_option_number {
- use std::fmt;
-
- use serde::de::{Deserializer, Error, Visitor};
-
- pub fn deserialize<'de, D: Deserializer<'de>>(
- deserializer: D,
- ) -> Result<Option<i64>, D::Error> {
- struct V;
-
- impl<'de> Visitor<'de> for V {
- type Value = Option<i64>;
-
- fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
- f.write_str("a numerical value")
- }
-
- fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
- v.parse().map_err(E::custom).map(Some)
- }
-
- fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
- Ok(None)
- }
-
- fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
- where
- D: Deserializer<'de>,
- {
- deserializer.deserialize_any(self)
- }
- }
-
- deserializer.deserialize_any(V)
- }
-}
-
-// parses a bool from a string OR bool
-pub mod serde_option_bool {
- use std::fmt;
-
- use serde::de::{Deserializer, Error, Visitor};
-
- pub fn deserialize<'de, D: Deserializer<'de>>(
- deserializer: D,
- ) -> Result<Option<bool>, D::Error> {
- struct V;
-
- impl<'de> Visitor<'de> for V {
- type Value = Option<bool>;
-
- fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
- f.write_str("a boolean-like value")
- }
-
- fn visit_bool<E: Error>(self, v: bool) -> Result<Self::Value, E> {
- Ok(Some(v))
- }
-
- fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
- super::parse_bool(v).map_err(E::custom).map(Some)
- }
-
- fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
- Ok(None)
- }
-
- fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
- where
- D: Deserializer<'de>,
- {
- deserializer.deserialize_any(self)
- }
- }
-
- deserializer.deserialize_any(V)
- }
-}
-
// parses a comma_separated list of strings
pub mod serde_option_conntrack_helpers {
use std::fmt;
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs v3 02/21] ve-config: move types to proxmox-network-types
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (5 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 01/21] config: use proxmox_serde perl helpers Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 03/21] sdn-types: initial commit Stefan Hanreich
` (67 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
Some of the types defined in this crate have been moved to
proxmox-network-types so they can be re-used across crates. This is a
preparation for the fabrics patch series, where those types will get
used by addtional, new, crates.
Remove the types that have been moved and adjust the imports for them
accordingly. This patch has no functional changes, except for one in
the firewall ipset type. There we had a blanket From implementation:
impl<T: Into<Cidr>> From<T> for IpsetAddress
This is no longer possible due to the orphan rule, so it has been
replaced with a simple From<Cidr> implementation. All call sites that
used the blanket implementation for the trait have been adjusted as
well.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
Cargo.toml | 2 +
proxmox-ve-config/Cargo.toml | 1 +
proxmox-ve-config/debian/control | 2 +
proxmox-ve-config/src/firewall/cluster.rs | 3 +-
proxmox-ve-config/src/firewall/ct_helper.rs | 2 +-
proxmox-ve-config/src/firewall/host.rs | 4 +-
.../src/firewall/types/address.rs | 1394 +----------------
proxmox-ve-config/src/firewall/types/alias.rs | 2 +-
proxmox-ve-config/src/firewall/types/ipset.rs | 8 +-
proxmox-ve-config/src/firewall/types/mod.rs | 1 -
proxmox-ve-config/src/firewall/types/rule.rs | 5 +-
.../src/firewall/types/rule_match.rs | 6 +-
proxmox-ve-config/src/guest/vm.rs | 96 +-
proxmox-ve-config/src/host/utils.rs | 2 +-
proxmox-ve-config/src/sdn/config.rs | 9 +-
proxmox-ve-config/src/sdn/ipam.rs | 11 +-
proxmox-ve-config/src/sdn/mod.rs | 3 +-
proxmox-ve-config/tests/sdn/main.rs | 11 +-
18 files changed, 62 insertions(+), 1500 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index dc7f312..b6e6df7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,3 +15,5 @@ homepage = "https://proxmox.com"
exclude = [ "debian" ]
rust-version = "1.82"
+[workspace.dependencies]
+proxmox-network-types = { version = "0.1" }
diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml
index e998b45..72fb627 100644
--- a/proxmox-ve-config/Cargo.toml
+++ b/proxmox-ve-config/Cargo.toml
@@ -18,6 +18,7 @@ serde_plain = "1"
serde_with = "3"
proxmox-serde = { version = "0.1.2", features = [ "perl" ]}
+proxmox-network-types = { workspace = true }
proxmox-schema = "4"
proxmox-sys = "0.6.4"
proxmox-sortable-macro = "0.1.3"
diff --git a/proxmox-ve-config/debian/control b/proxmox-ve-config/debian/control
index 2c43724..b76904a 100644
--- a/proxmox-ve-config/debian/control
+++ b/proxmox-ve-config/debian/control
@@ -9,6 +9,7 @@ Build-Depends-Arch: cargo:native <!nocheck>,
librust-anyhow-1+default-dev <!nocheck>,
librust-log-0.4+default-dev <!nocheck>,
librust-nix-0.26+default-dev <!nocheck>,
+ librust-proxmox-network-types-0.1+default-dev <!nocheck>,
librust-proxmox-schema-4+default-dev <!nocheck>,
librust-proxmox-serde-0.1+default-dev (>= 0.1.2-~~) <!nocheck>,
librust-proxmox-serde-0.1+perl-dev (>= 0.1.2-~~) <!nocheck>,
@@ -35,6 +36,7 @@ Depends:
librust-anyhow-1+default-dev,
librust-log-0.4+default-dev,
librust-nix-0.26+default-dev,
+ librust-proxmox-network-types-0.1+default-dev,
librust-proxmox-schema-4+default-dev,
librust-proxmox-serde-0.1+default-dev (>= 0.1.2-~~),
librust-proxmox-serde-0.1+perl-dev (>= 0.1.2-~~),
diff --git a/proxmox-ve-config/src/firewall/cluster.rs b/proxmox-ve-config/src/firewall/cluster.rs
index a775cd9..d588302 100644
--- a/proxmox-ve-config/src/firewall/cluster.rs
+++ b/proxmox-ve-config/src/firewall/cluster.rs
@@ -134,6 +134,8 @@ pub struct Options {
#[cfg(test)]
mod tests {
+ use proxmox_network_types::ip_address::Cidr;
+
use crate::firewall::types::{
address::IpList,
alias::{AliasName, AliasScope},
@@ -143,7 +145,6 @@ mod tests {
rule_match::{
Icmpv6, Icmpv6Code, IpAddrMatch, IpMatch, Ports, Protocol, RuleMatch, Tcp, Udp,
},
- Cidr,
};
use super::*;
diff --git a/proxmox-ve-config/src/firewall/ct_helper.rs b/proxmox-ve-config/src/firewall/ct_helper.rs
index 40e4fee..57fe9aa 100644
--- a/proxmox-ve-config/src/firewall/ct_helper.rs
+++ b/proxmox-ve-config/src/firewall/ct_helper.rs
@@ -3,7 +3,7 @@ use serde::Deserialize;
use std::collections::HashMap;
use std::sync::OnceLock;
-use crate::firewall::types::address::Family;
+use proxmox_network_types::ip_address::Family;
use crate::firewall::types::rule_match::{Ports, Protocol, Tcp, Udp};
#[derive(Clone, Debug, Deserialize)]
diff --git a/proxmox-ve-config/src/firewall/host.rs b/proxmox-ve-config/src/firewall/host.rs
index f7b02f9..d749442 100644
--- a/proxmox-ve-config/src/firewall/host.rs
+++ b/proxmox-ve-config/src/firewall/host.rs
@@ -4,13 +4,15 @@ use std::net::IpAddr;
use anyhow::{bail, Error};
use serde::Deserialize;
+use proxmox_network_types::ip_address::Cidr;
+
use crate::host::utils::{host_ips, network_interface_cidrs};
use proxmox_sys::nodename;
use crate::firewall::parse;
use crate::firewall::types::log::LogLevel;
use crate::firewall::types::rule::Direction;
-use crate::firewall::types::{Alias, Cidr, Rule};
+use crate::firewall::types::{Alias, Rule};
/// default setting for the enabled key
pub const HOST_ENABLED_DEFAULT: bool = true;
diff --git a/proxmox-ve-config/src/firewall/types/address.rs b/proxmox-ve-config/src/firewall/types/address.rs
index c218d37..3ed255c 100644
--- a/proxmox-ve-config/src/firewall/types/address.rs
+++ b/proxmox-ve-config/src/firewall/types/address.rs
@@ -1,600 +1,9 @@
-use std::fmt::{self, Display};
-use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
+use std::fmt;
use std::ops::Deref;
-use anyhow::{bail, format_err, Error};
-use serde_with::{DeserializeFromStr, SerializeDisplay};
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub enum Family {
- V4,
- V6,
-}
-
-impl Family {
- pub fn is_ipv4(&self) -> bool {
- *self == Self::V4
- }
-
- pub fn is_ipv6(&self) -> bool {
- *self == Self::V6
- }
-}
-
-impl fmt::Display for Family {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- match self {
- Family::V4 => f.write_str("Ipv4"),
- Family::V6 => f.write_str("Ipv6"),
- }
- }
-}
-
-#[derive(
- Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Hash, SerializeDisplay, DeserializeFromStr,
-)]
-pub enum Cidr {
- Ipv4(Ipv4Cidr),
- Ipv6(Ipv6Cidr),
-}
-
-impl Cidr {
- pub fn new_v4(addr: impl Into<Ipv4Addr>, mask: u8) -> Result<Self, Error> {
- Ok(Cidr::Ipv4(Ipv4Cidr::new(addr, mask)?))
- }
-
- pub fn new_v6(addr: impl Into<Ipv6Addr>, mask: u8) -> Result<Self, Error> {
- Ok(Cidr::Ipv6(Ipv6Cidr::new(addr, mask)?))
- }
-
- pub const fn family(&self) -> Family {
- match self {
- Cidr::Ipv4(_) => Family::V4,
- Cidr::Ipv6(_) => Family::V6,
- }
- }
-
- pub fn is_ipv4(&self) -> bool {
- matches!(self, Cidr::Ipv4(_))
- }
-
- pub fn is_ipv6(&self) -> bool {
- matches!(self, Cidr::Ipv6(_))
- }
-
- pub fn contains_address(&self, ip: &IpAddr) -> bool {
- match (self, ip) {
- (Cidr::Ipv4(cidr), IpAddr::V4(ip)) => cidr.contains_address(ip),
- (Cidr::Ipv6(cidr), IpAddr::V6(ip)) => cidr.contains_address(ip),
- _ => false,
- }
- }
-}
-
-impl fmt::Display for Cidr {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- match self {
- Self::Ipv4(ip) => f.write_str(ip.to_string().as_str()),
- Self::Ipv6(ip) => f.write_str(ip.to_string().as_str()),
- }
- }
-}
-
-impl std::str::FromStr for Cidr {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Error> {
- if let Ok(ip) = s.parse::<Ipv4Cidr>() {
- return Ok(Cidr::Ipv4(ip));
- }
-
- if let Ok(ip) = s.parse::<Ipv6Cidr>() {
- return Ok(Cidr::Ipv6(ip));
- }
-
- bail!("invalid ip address or CIDR: {s:?}");
- }
-}
-
-impl From<Ipv4Cidr> for Cidr {
- fn from(cidr: Ipv4Cidr) -> Self {
- Cidr::Ipv4(cidr)
- }
-}
-
-impl From<Ipv6Cidr> for Cidr {
- fn from(cidr: Ipv6Cidr) -> Self {
- Cidr::Ipv6(cidr)
- }
-}
-
-impl From<IpAddr> for Cidr {
- fn from(value: IpAddr) -> Self {
- match value {
- IpAddr::V4(addr) => Ipv4Cidr::from(addr).into(),
- IpAddr::V6(addr) => Ipv6Cidr::from(addr).into(),
- }
- }
-}
-
-const IPV4_LENGTH: u8 = 32;
-
-#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Hash, DeserializeFromStr)]
-pub struct Ipv4Cidr {
- addr: Ipv4Addr,
- mask: u8,
-}
-
-impl Ipv4Cidr {
- pub fn new(addr: impl Into<Ipv4Addr>, mask: u8) -> Result<Self, Error> {
- if mask > 32 {
- bail!("mask out of range for ipv4 cidr ({mask})");
- }
-
- Ok(Self {
- addr: addr.into(),
- mask,
- })
- }
-
- pub fn contains_address(&self, other: &Ipv4Addr) -> bool {
- let bits = u32::from_be_bytes(self.addr.octets());
- let other_bits = u32::from_be_bytes(other.octets());
-
- let shift_amount: u32 = IPV4_LENGTH.saturating_sub(self.mask).into();
-
- bits.checked_shr(shift_amount).unwrap_or(0)
- == other_bits.checked_shr(shift_amount).unwrap_or(0)
- }
-
- pub fn address(&self) -> &Ipv4Addr {
- &self.addr
- }
-
- pub fn mask(&self) -> u8 {
- self.mask
- }
-}
-
-impl<T: Into<Ipv4Addr>> From<T> for Ipv4Cidr {
- fn from(value: T) -> Self {
- Self {
- addr: value.into(),
- mask: 32,
- }
- }
-}
-
-impl std::str::FromStr for Ipv4Cidr {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Error> {
- Ok(match s.find('/') {
- None => Self {
- addr: s.parse()?,
- mask: 32,
- },
- Some(pos) => {
- let mask: u8 = s[(pos + 1)..]
- .parse()
- .map_err(|_| format_err!("invalid mask in ipv4 cidr: {s:?}"))?;
-
- Self::new(s[..pos].parse::<Ipv4Addr>()?, mask)?
- }
- })
- }
-}
-
-impl fmt::Display for Ipv4Cidr {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "{}/{}", &self.addr, self.mask)
- }
-}
-
-const IPV6_LENGTH: u8 = 128;
-
-#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Hash, DeserializeFromStr)]
-pub struct Ipv6Cidr {
- addr: Ipv6Addr,
- mask: u8,
-}
-
-impl Ipv6Cidr {
- pub fn new(addr: impl Into<Ipv6Addr>, mask: u8) -> Result<Self, Error> {
- if mask > IPV6_LENGTH {
- bail!("mask out of range for ipv6 cidr");
- }
-
- Ok(Self {
- addr: addr.into(),
- mask,
- })
- }
-
- pub fn contains_address(&self, other: &Ipv6Addr) -> bool {
- let bits = u128::from_be_bytes(self.addr.octets());
- let other_bits = u128::from_be_bytes(other.octets());
-
- let shift_amount: u32 = IPV6_LENGTH.saturating_sub(self.mask).into();
-
- bits.checked_shr(shift_amount).unwrap_or(0)
- == other_bits.checked_shr(shift_amount).unwrap_or(0)
- }
-
- pub fn address(&self) -> &Ipv6Addr {
- &self.addr
- }
-
- pub fn mask(&self) -> u8 {
- self.mask
- }
-}
-
-impl std::str::FromStr for Ipv6Cidr {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Error> {
- Ok(match s.find('/') {
- None => Self {
- addr: s.parse()?,
- mask: 128,
- },
- Some(pos) => {
- let mask: u8 = s[(pos + 1)..]
- .parse()
- .map_err(|_| format_err!("invalid mask in ipv6 cidr: {s:?}"))?;
-
- Self::new(s[..pos].parse::<Ipv6Addr>()?, mask)?
- }
- })
- }
-}
-
-impl fmt::Display for Ipv6Cidr {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "{}/{}", &self.addr, self.mask)
- }
-}
-
-impl<T: Into<Ipv6Addr>> From<T> for Ipv6Cidr {
- fn from(addr: T) -> Self {
- Self {
- addr: addr.into(),
- mask: 128,
- }
- }
-}
-
-#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
-pub enum IpRangeError {
- MismatchedFamilies,
- StartGreaterThanLast,
- InvalidFormat,
-}
-
-impl std::error::Error for IpRangeError {}
-
-impl Display for IpRangeError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.write_str(match self {
- IpRangeError::MismatchedFamilies => "mismatched ip address families",
- IpRangeError::StartGreaterThanLast => "start is greater than last",
- IpRangeError::InvalidFormat => "invalid ip range format",
- })
- }
-}
-
-/// Represents a range of IPv4 or IPv6 addresses.
-///
-/// For more information see [`AddressRange`]
-#[derive(
- Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, SerializeDisplay, DeserializeFromStr,
-)]
-pub enum IpRange {
- V4(AddressRange<Ipv4Addr>),
- V6(AddressRange<Ipv6Addr>),
-}
-
-impl IpRange {
- /// Returns the family of the IpRange.
- pub fn family(&self) -> Family {
- match self {
- IpRange::V4(_) => Family::V4,
- IpRange::V6(_) => Family::V6,
- }
- }
-
- /// Creates a new [`IpRange`] from two [`IpAddr`].
- ///
- /// # Errors
- ///
- /// This function will return an error if start and last IP address are not from the same family.
- pub fn new(start: impl Into<IpAddr>, last: impl Into<IpAddr>) -> Result<Self, IpRangeError> {
- match (start.into(), last.into()) {
- (IpAddr::V4(start), IpAddr::V4(last)) => Self::new_v4(start, last),
- (IpAddr::V6(start), IpAddr::V6(last)) => Self::new_v6(start, last),
- _ => Err(IpRangeError::MismatchedFamilies),
- }
- }
-
- /// construct a new Ipv4 Range
- pub fn new_v4(
- start: impl Into<Ipv4Addr>,
- last: impl Into<Ipv4Addr>,
- ) -> Result<Self, IpRangeError> {
- Ok(IpRange::V4(AddressRange::new_v4(start, last)?))
- }
-
- pub fn new_v6(
- start: impl Into<Ipv6Addr>,
- last: impl Into<Ipv6Addr>,
- ) -> Result<Self, IpRangeError> {
- Ok(IpRange::V6(AddressRange::new_v6(start, last)?))
- }
-
- /// Converts an IpRange into the minimal amount of CIDRs.
- ///
- /// see the concrete implementations of [`AddressRange<Ipv4Addr>`] or [`AddressRange<Ipv6Addr>`]
- /// respectively
- pub fn to_cidrs(&self) -> Vec<Cidr> {
- match self {
- IpRange::V4(range) => range.to_cidrs().into_iter().map(Cidr::from).collect(),
- IpRange::V6(range) => range.to_cidrs().into_iter().map(Cidr::from).collect(),
- }
- }
-}
-
-impl std::str::FromStr for IpRange {
- type Err = IpRangeError;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- if let Ok(range) = s.parse() {
- return Ok(IpRange::V4(range));
- }
-
- if let Ok(range) = s.parse() {
- return Ok(IpRange::V6(range));
- }
-
- Err(IpRangeError::InvalidFormat)
- }
-}
-
-impl fmt::Display for IpRange {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- IpRange::V4(range) => range.fmt(f),
- IpRange::V6(range) => range.fmt(f),
- }
- }
-}
-
-/// Represents a range of IP addresses from start to last.
-///
-/// This type is for encapsulation purposes for the [`IpRange`] enum and should be instantiated via
-/// that enum.
-///
-/// # Invariants
-///
-/// * start and last have the same IP address family
-/// * start is less than or equal to last
-///
-/// # Textual representation
-///
-/// Two IP addresses separated by a hyphen, e.g.: `127.0.0.1-127.0.0.255`
-#[derive(
- Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, SerializeDisplay, DeserializeFromStr,
-)]
-pub struct AddressRange<T> {
- start: T,
- last: T,
-}
-
-impl AddressRange<Ipv4Addr> {
- pub(crate) fn new_v4(
- start: impl Into<Ipv4Addr>,
- last: impl Into<Ipv4Addr>,
- ) -> Result<AddressRange<Ipv4Addr>, IpRangeError> {
- let (start, last) = (start.into(), last.into());
-
- if start > last {
- return Err(IpRangeError::StartGreaterThanLast);
- }
-
- Ok(Self { start, last })
- }
-
- /// Returns the minimum amount of CIDRs that exactly represent the range
- ///
- /// The idea behind this algorithm is as follows:
- ///
- /// Start iterating with current = start of the IP range
- ///
- /// Find two netmasks
- /// * The largest CIDR that the current IP can be the first of
- /// * The largest CIDR that *only* contains IPs from current - last
- ///
- /// Add the smaller of the two CIDRs to our result and current to the first IP that is in
- /// the range but not in the CIDR we just added. Proceed until we reached the last of the IP
- /// range.
- ///
- pub fn to_cidrs(&self) -> Vec<Ipv4Cidr> {
- let mut cidrs = Vec::new();
-
- let mut current = u32::from_be_bytes(self.start.octets());
- let last = u32::from_be_bytes(self.last.octets());
-
- if current == last {
- // valid Ipv4 since netmask is 32
- cidrs.push(Ipv4Cidr::new(current, 32).unwrap());
- return cidrs;
- }
-
- // special case this, since this is the only possibility of overflow
- // when calculating delta_min_mask - makes everything a lot easier
- if current == u32::MIN && last == u32::MAX {
- // valid Ipv4 since it is `0.0.0.0/0`
- cidrs.push(Ipv4Cidr::new(current, 0).unwrap());
- return cidrs;
- }
-
- while current <= last {
- // netmask of largest CIDR that current IP can be the first of
- // cast is safe, because trailing zeroes can at most be 32
- let current_max_mask = IPV4_LENGTH - (current.trailing_zeros() as u8);
-
- // netmask of largest CIDR that *only* contains IPs of the remaining range
- // is at most 32 due to unwrap_or returning 32 and ilog2 being at most 31
- let delta_min_mask = ((last - current) + 1) // safe due to special case above
- .checked_ilog2() // should never occur due to special case, but for good measure
- .map(|mask| IPV4_LENGTH - mask as u8)
- .unwrap_or(IPV4_LENGTH);
-
- // at most 32, due to current/delta being at most 32
- let netmask = u8::max(current_max_mask, delta_min_mask);
-
- // netmask is at most 32, therefore safe to unwrap
- cidrs.push(Ipv4Cidr::new(current, netmask).unwrap());
-
- let delta = 2u32.saturating_pow((IPV4_LENGTH - netmask).into());
-
- if let Some(result) = current.checked_add(delta) {
- current = result
- } else {
- // we reached the end of IP address space
- break;
- }
- }
-
- cidrs
- }
-}
-
-impl AddressRange<Ipv6Addr> {
- pub(crate) fn new_v6(
- start: impl Into<Ipv6Addr>,
- last: impl Into<Ipv6Addr>,
- ) -> Result<AddressRange<Ipv6Addr>, IpRangeError> {
- let (start, last) = (start.into(), last.into());
-
- if start > last {
- return Err(IpRangeError::StartGreaterThanLast);
- }
-
- Ok(Self { start, last })
- }
-
- /// Returns the minimum amount of CIDRs that exactly represent the [`AddressRange`].
- ///
- /// This function works analogous to the IPv4 version, please refer to the respective
- /// documentation of [`AddressRange<Ipv4Addr>`]
- pub fn to_cidrs(&self) -> Vec<Ipv6Cidr> {
- let mut cidrs = Vec::new();
-
- let mut current = u128::from_be_bytes(self.start.octets());
- let last = u128::from_be_bytes(self.last.octets());
-
- if current == last {
- // valid Ipv6 since netmask is 128
- cidrs.push(Ipv6Cidr::new(current, 128).unwrap());
- return cidrs;
- }
-
- // special case this, since this is the only possibility of overflow
- // when calculating delta_min_mask - makes everything a lot easier
- if current == u128::MIN && last == u128::MAX {
- // valid Ipv6 since it is `::/0`
- cidrs.push(Ipv6Cidr::new(current, 0).unwrap());
- return cidrs;
- }
-
- while current <= last {
- // netmask of largest CIDR that current IP can be the first of
- // cast is safe, because trailing zeroes can at most be 128
- let current_max_mask = IPV6_LENGTH - (current.trailing_zeros() as u8);
-
- // netmask of largest CIDR that *only* contains IPs of the remaining range
- // is at most 128 due to unwrap_or returning 128 and ilog2 being at most 31
- let delta_min_mask = ((last - current) + 1) // safe due to special case above
- .checked_ilog2() // should never occur due to special case, but for good measure
- .map(|mask| IPV6_LENGTH - mask as u8)
- .unwrap_or(IPV6_LENGTH);
-
- // at most 128, due to current/delta being at most 128
- let netmask = u8::max(current_max_mask, delta_min_mask);
-
- // netmask is at most 128, therefore safe to unwrap
- cidrs.push(Ipv6Cidr::new(current, netmask).unwrap());
-
- let delta = 2u128.saturating_pow((IPV6_LENGTH - netmask).into());
-
- if let Some(result) = current.checked_add(delta) {
- current = result
- } else {
- // we reached the end of IP address space
- break;
- }
- }
-
- cidrs
- }
-}
-
-impl<T> AddressRange<T> {
- pub fn start(&self) -> &T {
- &self.start
- }
-
- pub fn last(&self) -> &T {
- &self.last
- }
-}
-
-impl std::str::FromStr for AddressRange<Ipv4Addr> {
- type Err = IpRangeError;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- if let Some((start, last)) = s.split_once('-') {
- let start_address = start
- .parse::<Ipv4Addr>()
- .map_err(|_| IpRangeError::InvalidFormat)?;
-
- let last_address = last
- .parse::<Ipv4Addr>()
- .map_err(|_| IpRangeError::InvalidFormat)?;
-
- return Self::new_v4(start_address, last_address);
- }
-
- Err(IpRangeError::InvalidFormat)
- }
-}
-
-impl std::str::FromStr for AddressRange<Ipv6Addr> {
- type Err = IpRangeError;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- if let Some((start, last)) = s.split_once('-') {
- let start_address = start
- .parse::<Ipv6Addr>()
- .map_err(|_| IpRangeError::InvalidFormat)?;
-
- let last_address = last
- .parse::<Ipv6Addr>()
- .map_err(|_| IpRangeError::InvalidFormat)?;
-
- return Self::new_v6(start_address, last_address);
- }
-
- Err(IpRangeError::InvalidFormat)
- }
-}
-
-impl<T: fmt::Display> fmt::Display for AddressRange<T> {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "{}-{}", self.start, self.last)
- }
-}
+use anyhow::{bail, Error};
+use proxmox_network_types::ip_address::{Cidr, Family, IpRange};
+use serde_with::DeserializeFromStr;
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
@@ -741,84 +150,6 @@ impl IpList {
#[cfg(test)]
mod tests {
use super::*;
- use std::net::{Ipv4Addr, Ipv6Addr};
-
- #[test]
- fn test_v4_cidr() {
- let mut cidr: Ipv4Cidr = "0.0.0.0/0".parse().expect("valid IPv4 CIDR");
-
- assert_eq!(cidr.addr, Ipv4Addr::new(0, 0, 0, 0));
- assert_eq!(cidr.mask, 0);
-
- assert!(cidr.contains_address(&Ipv4Addr::new(0, 0, 0, 0)));
- assert!(cidr.contains_address(&Ipv4Addr::new(255, 255, 255, 255)));
-
- cidr = "192.168.100.1".parse().expect("valid IPv4 CIDR");
-
- assert_eq!(cidr.addr, Ipv4Addr::new(192, 168, 100, 1));
- assert_eq!(cidr.mask, 32);
-
- assert!(cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 1)));
- assert!(!cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 2)));
- assert!(!cidr.contains_address(&Ipv4Addr::new(192, 168, 100, 0)));
-
- cidr = "10.100.5.0/24".parse().expect("valid IPv4 CIDR");
-
- assert_eq!(cidr.mask, 24);
-
- assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 0)));
- assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 1)));
- assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 100)));
- assert!(cidr.contains_address(&Ipv4Addr::new(10, 100, 5, 255)));
- assert!(!cidr.contains_address(&Ipv4Addr::new(10, 100, 4, 255)));
- assert!(!cidr.contains_address(&Ipv4Addr::new(10, 100, 6, 0)));
-
- "0.0.0.0/-1".parse::<Ipv4Cidr>().unwrap_err();
- "0.0.0.0/33".parse::<Ipv4Cidr>().unwrap_err();
- "256.256.256.256/10".parse::<Ipv4Cidr>().unwrap_err();
-
- "fe80::1/64".parse::<Ipv4Cidr>().unwrap_err();
- "qweasd".parse::<Ipv4Cidr>().unwrap_err();
- "".parse::<Ipv4Cidr>().unwrap_err();
- }
-
- #[test]
- fn test_v6_cidr() {
- let mut cidr: Ipv6Cidr = "abab::1/64".parse().expect("valid IPv6 CIDR");
-
- assert_eq!(cidr.addr, Ipv6Addr::new(0xABAB, 0, 0, 0, 0, 0, 0, 1));
- assert_eq!(cidr.mask, 64);
-
- assert!(cidr.contains_address(&Ipv6Addr::new(0xABAB, 0, 0, 0, 0, 0, 0, 0)));
- assert!(cidr.contains_address(&Ipv6Addr::new(
- 0xABAB, 0, 0, 0, 0xAAAA, 0xAAAA, 0xAAAA, 0xAAAA
- )));
- assert!(cidr.contains_address(&Ipv6Addr::new(
- 0xABAB, 0, 0, 0, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
- )));
- assert!(!cidr.contains_address(&Ipv6Addr::new(0xABAB, 0, 0, 1, 0, 0, 0, 0)));
- assert!(!cidr.contains_address(&Ipv6Addr::new(
- 0xABAA, 0, 0, 0, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
- )));
-
- cidr = "eeee::1".parse().expect("valid IPv6 CIDR");
-
- assert_eq!(cidr.mask, 128);
-
- assert!(cidr.contains_address(&Ipv6Addr::new(0xEEEE, 0, 0, 0, 0, 0, 0, 1)));
- assert!(!cidr.contains_address(&Ipv6Addr::new(
- 0xEEED, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF
- )));
- assert!(!cidr.contains_address(&Ipv6Addr::new(0xEEEE, 0, 0, 0, 0, 0, 0, 0)));
-
- "eeee::1/-1".parse::<Ipv6Cidr>().unwrap_err();
- "eeee::1/129".parse::<Ipv6Cidr>().unwrap_err();
- "gggg::1/64".parse::<Ipv6Cidr>().unwrap_err();
-
- "192.168.0.1".parse::<Ipv6Cidr>().unwrap_err();
- "qweasd".parse::<Ipv6Cidr>().unwrap_err();
- "".parse::<Ipv6Cidr>().unwrap_err();
- }
#[test]
fn test_parse_ip_entry() {
@@ -942,721 +273,4 @@ mod tests {
])
.expect_err("cannot mix ip families in ip list");
}
-
- #[test]
- fn test_ip_range() {
- IpRange::new([10, 0, 0, 2], [10, 0, 0, 1]).unwrap_err();
-
- IpRange::new(
- [0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x1000],
- [0x2001, 0x0db8, 0, 0, 0, 0, 0, 0],
- )
- .unwrap_err();
-
- let v4_range = IpRange::new([10, 0, 0, 0], [10, 0, 0, 100]).unwrap();
- assert_eq!(v4_range.family(), Family::V4);
-
- let v6_range = IpRange::new(
- [0x2001, 0x0db8, 0, 0, 0, 0, 0, 0],
- [0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x1000],
- )
- .unwrap();
- assert_eq!(v6_range.family(), Family::V6);
-
- "10.0.0.1-10.0.0.100".parse::<IpRange>().unwrap();
- "2001:db8::1-2001:db8::f".parse::<IpRange>().unwrap();
-
- "10.0.0.1-2001:db8::1000".parse::<IpRange>().unwrap_err();
- "2001:db8::1-192.168.0.2".parse::<IpRange>().unwrap_err();
-
- "10.0.0.1-10.0.0.0".parse::<IpRange>().unwrap_err();
- "2001:db8::1-2001:db8::0".parse::<IpRange>().unwrap_err();
- }
-
- #[test]
- fn test_ipv4_to_cidrs() {
- let range = AddressRange::new_v4([192, 168, 0, 100], [192, 168, 0, 100]).unwrap();
-
- assert_eq!(
- [Ipv4Cidr::new([192, 168, 0, 100], 32).unwrap()],
- range.to_cidrs().as_slice()
- );
-
- let range = AddressRange::new_v4([192, 168, 0, 100], [192, 168, 0, 200]).unwrap();
-
- assert_eq!(
- [
- Ipv4Cidr::new([192, 168, 0, 100], 30).unwrap(),
- Ipv4Cidr::new([192, 168, 0, 104], 29).unwrap(),
- Ipv4Cidr::new([192, 168, 0, 112], 28).unwrap(),
- Ipv4Cidr::new([192, 168, 0, 128], 26).unwrap(),
- Ipv4Cidr::new([192, 168, 0, 192], 29).unwrap(),
- Ipv4Cidr::new([192, 168, 0, 200], 32).unwrap(),
- ],
- range.to_cidrs().as_slice()
- );
-
- let range = AddressRange::new_v4([192, 168, 0, 101], [192, 168, 0, 200]).unwrap();
-
- assert_eq!(
- [
- Ipv4Cidr::new([192, 168, 0, 101], 32).unwrap(),
- Ipv4Cidr::new([192, 168, 0, 102], 31).unwrap(),
- Ipv4Cidr::new([192, 168, 0, 104], 29).unwrap(),
- Ipv4Cidr::new([192, 168, 0, 112], 28).unwrap(),
- Ipv4Cidr::new([192, 168, 0, 128], 26).unwrap(),
- Ipv4Cidr::new([192, 168, 0, 192], 29).unwrap(),
- Ipv4Cidr::new([192, 168, 0, 200], 32).unwrap(),
- ],
- range.to_cidrs().as_slice()
- );
-
- let range = AddressRange::new_v4([192, 168, 0, 101], [192, 168, 0, 101]).unwrap();
-
- assert_eq!(
- [Ipv4Cidr::new([192, 168, 0, 101], 32).unwrap()],
- range.to_cidrs().as_slice()
- );
-
- let range = AddressRange::new_v4([192, 168, 0, 101], [192, 168, 0, 201]).unwrap();
-
- assert_eq!(
- [
- Ipv4Cidr::new([192, 168, 0, 101], 32).unwrap(),
- Ipv4Cidr::new([192, 168, 0, 102], 31).unwrap(),
- Ipv4Cidr::new([192, 168, 0, 104], 29).unwrap(),
- Ipv4Cidr::new([192, 168, 0, 112], 28).unwrap(),
- Ipv4Cidr::new([192, 168, 0, 128], 26).unwrap(),
- Ipv4Cidr::new([192, 168, 0, 192], 29).unwrap(),
- Ipv4Cidr::new([192, 168, 0, 200], 31).unwrap(),
- ],
- range.to_cidrs().as_slice()
- );
-
- let range = AddressRange::new_v4([192, 168, 0, 0], [192, 168, 0, 255]).unwrap();
-
- assert_eq!(
- [Ipv4Cidr::new([192, 168, 0, 0], 24).unwrap(),],
- range.to_cidrs().as_slice()
- );
-
- let range = AddressRange::new_v4([0, 0, 0, 0], [255, 255, 255, 255]).unwrap();
-
- assert_eq!(
- [Ipv4Cidr::new([0, 0, 0, 0], 0).unwrap(),],
- range.to_cidrs().as_slice()
- );
-
- let range = AddressRange::new_v4([0, 0, 0, 1], [255, 255, 255, 255]).unwrap();
-
- assert_eq!(
- [
- Ipv4Cidr::new([0, 0, 0, 1], 32).unwrap(),
- Ipv4Cidr::new([0, 0, 0, 2], 31).unwrap(),
- Ipv4Cidr::new([0, 0, 0, 4], 30).unwrap(),
- Ipv4Cidr::new([0, 0, 0, 8], 29).unwrap(),
- Ipv4Cidr::new([0, 0, 0, 16], 28).unwrap(),
- Ipv4Cidr::new([0, 0, 0, 32], 27).unwrap(),
- Ipv4Cidr::new([0, 0, 0, 64], 26).unwrap(),
- Ipv4Cidr::new([0, 0, 0, 128], 25).unwrap(),
- Ipv4Cidr::new([0, 0, 1, 0], 24).unwrap(),
- Ipv4Cidr::new([0, 0, 2, 0], 23).unwrap(),
- Ipv4Cidr::new([0, 0, 4, 0], 22).unwrap(),
- Ipv4Cidr::new([0, 0, 8, 0], 21).unwrap(),
- Ipv4Cidr::new([0, 0, 16, 0], 20).unwrap(),
- Ipv4Cidr::new([0, 0, 32, 0], 19).unwrap(),
- Ipv4Cidr::new([0, 0, 64, 0], 18).unwrap(),
- Ipv4Cidr::new([0, 0, 128, 0], 17).unwrap(),
- Ipv4Cidr::new([0, 1, 0, 0], 16).unwrap(),
- Ipv4Cidr::new([0, 2, 0, 0], 15).unwrap(),
- Ipv4Cidr::new([0, 4, 0, 0], 14).unwrap(),
- Ipv4Cidr::new([0, 8, 0, 0], 13).unwrap(),
- Ipv4Cidr::new([0, 16, 0, 0], 12).unwrap(),
- Ipv4Cidr::new([0, 32, 0, 0], 11).unwrap(),
- Ipv4Cidr::new([0, 64, 0, 0], 10).unwrap(),
- Ipv4Cidr::new([0, 128, 0, 0], 9).unwrap(),
- Ipv4Cidr::new([1, 0, 0, 0], 8).unwrap(),
- Ipv4Cidr::new([2, 0, 0, 0], 7).unwrap(),
- Ipv4Cidr::new([4, 0, 0, 0], 6).unwrap(),
- Ipv4Cidr::new([8, 0, 0, 0], 5).unwrap(),
- Ipv4Cidr::new([16, 0, 0, 0], 4).unwrap(),
- Ipv4Cidr::new([32, 0, 0, 0], 3).unwrap(),
- Ipv4Cidr::new([64, 0, 0, 0], 2).unwrap(),
- Ipv4Cidr::new([128, 0, 0, 0], 1).unwrap(),
- ],
- range.to_cidrs().as_slice()
- );
-
- let range = AddressRange::new_v4([0, 0, 0, 0], [255, 255, 255, 254]).unwrap();
-
- assert_eq!(
- [
- Ipv4Cidr::new([0, 0, 0, 0], 1).unwrap(),
- Ipv4Cidr::new([128, 0, 0, 0], 2).unwrap(),
- Ipv4Cidr::new([192, 0, 0, 0], 3).unwrap(),
- Ipv4Cidr::new([224, 0, 0, 0], 4).unwrap(),
- Ipv4Cidr::new([240, 0, 0, 0], 5).unwrap(),
- Ipv4Cidr::new([248, 0, 0, 0], 6).unwrap(),
- Ipv4Cidr::new([252, 0, 0, 0], 7).unwrap(),
- Ipv4Cidr::new([254, 0, 0, 0], 8).unwrap(),
- Ipv4Cidr::new([255, 0, 0, 0], 9).unwrap(),
- Ipv4Cidr::new([255, 128, 0, 0], 10).unwrap(),
- Ipv4Cidr::new([255, 192, 0, 0], 11).unwrap(),
- Ipv4Cidr::new([255, 224, 0, 0], 12).unwrap(),
- Ipv4Cidr::new([255, 240, 0, 0], 13).unwrap(),
- Ipv4Cidr::new([255, 248, 0, 0], 14).unwrap(),
- Ipv4Cidr::new([255, 252, 0, 0], 15).unwrap(),
- Ipv4Cidr::new([255, 254, 0, 0], 16).unwrap(),
- Ipv4Cidr::new([255, 255, 0, 0], 17).unwrap(),
- Ipv4Cidr::new([255, 255, 128, 0], 18).unwrap(),
- Ipv4Cidr::new([255, 255, 192, 0], 19).unwrap(),
- Ipv4Cidr::new([255, 255, 224, 0], 20).unwrap(),
- Ipv4Cidr::new([255, 255, 240, 0], 21).unwrap(),
- Ipv4Cidr::new([255, 255, 248, 0], 22).unwrap(),
- Ipv4Cidr::new([255, 255, 252, 0], 23).unwrap(),
- Ipv4Cidr::new([255, 255, 254, 0], 24).unwrap(),
- Ipv4Cidr::new([255, 255, 255, 0], 25).unwrap(),
- Ipv4Cidr::new([255, 255, 255, 128], 26).unwrap(),
- Ipv4Cidr::new([255, 255, 255, 192], 27).unwrap(),
- Ipv4Cidr::new([255, 255, 255, 224], 28).unwrap(),
- Ipv4Cidr::new([255, 255, 255, 240], 29).unwrap(),
- Ipv4Cidr::new([255, 255, 255, 248], 30).unwrap(),
- Ipv4Cidr::new([255, 255, 255, 252], 31).unwrap(),
- Ipv4Cidr::new([255, 255, 255, 254], 32).unwrap(),
- ],
- range.to_cidrs().as_slice()
- );
-
- let range = AddressRange::new_v4([0, 0, 0, 0], [0, 0, 0, 0]).unwrap();
-
- assert_eq!(
- [Ipv4Cidr::new([0, 0, 0, 0], 32).unwrap(),],
- range.to_cidrs().as_slice()
- );
-
- let range = AddressRange::new_v4([255, 255, 255, 255], [255, 255, 255, 255]).unwrap();
-
- assert_eq!(
- [Ipv4Cidr::new([255, 255, 255, 255], 32).unwrap(),],
- range.to_cidrs().as_slice()
- );
- }
-
- #[test]
- fn test_ipv6_to_cidrs() {
- let range = AddressRange::new_v6(
- [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1000],
- [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1000],
- )
- .unwrap();
-
- assert_eq!(
- [Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1000], 128).unwrap()],
- range.to_cidrs().as_slice()
- );
-
- let range = AddressRange::new_v6(
- [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1000],
- [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x2000],
- )
- .unwrap();
-
- assert_eq!(
- [
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1000], 116).unwrap(),
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x2000], 128).unwrap(),
- ],
- range.to_cidrs().as_slice()
- );
-
- let range = AddressRange::new_v6(
- [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1001],
- [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x2000],
- )
- .unwrap();
-
- assert_eq!(
- [
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1001], 128).unwrap(),
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1002], 127).unwrap(),
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1004], 126).unwrap(),
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1008], 125).unwrap(),
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1010], 124).unwrap(),
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1020], 123).unwrap(),
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1040], 122).unwrap(),
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1080], 121).unwrap(),
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1100], 120).unwrap(),
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1200], 119).unwrap(),
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1400], 118).unwrap(),
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1800], 117).unwrap(),
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x2000], 128).unwrap(),
- ],
- range.to_cidrs().as_slice()
- );
-
- let range = AddressRange::new_v6(
- [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1001],
- [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1001],
- )
- .unwrap();
-
- assert_eq!(
- [Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1001], 128).unwrap(),],
- range.to_cidrs().as_slice()
- );
-
- let range = AddressRange::new_v6(
- [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1001],
- [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x2001],
- )
- .unwrap();
-
- assert_eq!(
- [
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1001], 128).unwrap(),
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1002], 127).unwrap(),
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1004], 126).unwrap(),
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1008], 125).unwrap(),
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1010], 124).unwrap(),
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1020], 123).unwrap(),
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1040], 122).unwrap(),
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1080], 121).unwrap(),
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1100], 120).unwrap(),
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1200], 119).unwrap(),
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1400], 118).unwrap(),
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x1800], 117).unwrap(),
- Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0x2000], 127).unwrap(),
- ],
- range.to_cidrs().as_slice()
- );
-
- let range = AddressRange::new_v6(
- [0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0],
- [0x2001, 0x0DB8, 0, 0, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF],
- )
- .unwrap();
-
- assert_eq!(
- [Ipv6Cidr::new([0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0], 64).unwrap()],
- range.to_cidrs().as_slice()
- );
-
- let range = AddressRange::new_v6(
- [0, 0, 0, 0, 0, 0, 0, 0],
- [
- 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
- ],
- )
- .unwrap();
-
- assert_eq!(
- [Ipv6Cidr::new([0, 0, 0, 0, 0, 0, 0, 0], 0).unwrap(),],
- range.to_cidrs().as_slice()
- );
-
- let range = AddressRange::new_v6(
- [0, 0, 0, 0, 0, 0, 0, 0x0001],
- [
- 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
- ],
- )
- .unwrap();
-
- assert_eq!(
- [
- "::1/128".parse::<Ipv6Cidr>().unwrap(),
- "::2/127".parse::<Ipv6Cidr>().unwrap(),
- "::4/126".parse::<Ipv6Cidr>().unwrap(),
- "::8/125".parse::<Ipv6Cidr>().unwrap(),
- "::10/124".parse::<Ipv6Cidr>().unwrap(),
- "::20/123".parse::<Ipv6Cidr>().unwrap(),
- "::40/122".parse::<Ipv6Cidr>().unwrap(),
- "::80/121".parse::<Ipv6Cidr>().unwrap(),
- "::100/120".parse::<Ipv6Cidr>().unwrap(),
- "::200/119".parse::<Ipv6Cidr>().unwrap(),
- "::400/118".parse::<Ipv6Cidr>().unwrap(),
- "::800/117".parse::<Ipv6Cidr>().unwrap(),
- "::1000/116".parse::<Ipv6Cidr>().unwrap(),
- "::2000/115".parse::<Ipv6Cidr>().unwrap(),
- "::4000/114".parse::<Ipv6Cidr>().unwrap(),
- "::8000/113".parse::<Ipv6Cidr>().unwrap(),
- "::1:0/112".parse::<Ipv6Cidr>().unwrap(),
- "::2:0/111".parse::<Ipv6Cidr>().unwrap(),
- "::4:0/110".parse::<Ipv6Cidr>().unwrap(),
- "::8:0/109".parse::<Ipv6Cidr>().unwrap(),
- "::10:0/108".parse::<Ipv6Cidr>().unwrap(),
- "::20:0/107".parse::<Ipv6Cidr>().unwrap(),
- "::40:0/106".parse::<Ipv6Cidr>().unwrap(),
- "::80:0/105".parse::<Ipv6Cidr>().unwrap(),
- "::100:0/104".parse::<Ipv6Cidr>().unwrap(),
- "::200:0/103".parse::<Ipv6Cidr>().unwrap(),
- "::400:0/102".parse::<Ipv6Cidr>().unwrap(),
- "::800:0/101".parse::<Ipv6Cidr>().unwrap(),
- "::1000:0/100".parse::<Ipv6Cidr>().unwrap(),
- "::2000:0/99".parse::<Ipv6Cidr>().unwrap(),
- "::4000:0/98".parse::<Ipv6Cidr>().unwrap(),
- "::8000:0/97".parse::<Ipv6Cidr>().unwrap(),
- "::1:0:0/96".parse::<Ipv6Cidr>().unwrap(),
- "::2:0:0/95".parse::<Ipv6Cidr>().unwrap(),
- "::4:0:0/94".parse::<Ipv6Cidr>().unwrap(),
- "::8:0:0/93".parse::<Ipv6Cidr>().unwrap(),
- "::10:0:0/92".parse::<Ipv6Cidr>().unwrap(),
- "::20:0:0/91".parse::<Ipv6Cidr>().unwrap(),
- "::40:0:0/90".parse::<Ipv6Cidr>().unwrap(),
- "::80:0:0/89".parse::<Ipv6Cidr>().unwrap(),
- "::100:0:0/88".parse::<Ipv6Cidr>().unwrap(),
- "::200:0:0/87".parse::<Ipv6Cidr>().unwrap(),
- "::400:0:0/86".parse::<Ipv6Cidr>().unwrap(),
- "::800:0:0/85".parse::<Ipv6Cidr>().unwrap(),
- "::1000:0:0/84".parse::<Ipv6Cidr>().unwrap(),
- "::2000:0:0/83".parse::<Ipv6Cidr>().unwrap(),
- "::4000:0:0/82".parse::<Ipv6Cidr>().unwrap(),
- "::8000:0:0/81".parse::<Ipv6Cidr>().unwrap(),
- "::1:0:0:0/80".parse::<Ipv6Cidr>().unwrap(),
- "::2:0:0:0/79".parse::<Ipv6Cidr>().unwrap(),
- "::4:0:0:0/78".parse::<Ipv6Cidr>().unwrap(),
- "::8:0:0:0/77".parse::<Ipv6Cidr>().unwrap(),
- "::10:0:0:0/76".parse::<Ipv6Cidr>().unwrap(),
- "::20:0:0:0/75".parse::<Ipv6Cidr>().unwrap(),
- "::40:0:0:0/74".parse::<Ipv6Cidr>().unwrap(),
- "::80:0:0:0/73".parse::<Ipv6Cidr>().unwrap(),
- "::100:0:0:0/72".parse::<Ipv6Cidr>().unwrap(),
- "::200:0:0:0/71".parse::<Ipv6Cidr>().unwrap(),
- "::400:0:0:0/70".parse::<Ipv6Cidr>().unwrap(),
- "::800:0:0:0/69".parse::<Ipv6Cidr>().unwrap(),
- "::1000:0:0:0/68".parse::<Ipv6Cidr>().unwrap(),
- "::2000:0:0:0/67".parse::<Ipv6Cidr>().unwrap(),
- "::4000:0:0:0/66".parse::<Ipv6Cidr>().unwrap(),
- "::8000:0:0:0/65".parse::<Ipv6Cidr>().unwrap(),
- "0:0:0:1::/64".parse::<Ipv6Cidr>().unwrap(),
- "0:0:0:2::/63".parse::<Ipv6Cidr>().unwrap(),
- "0:0:0:4::/62".parse::<Ipv6Cidr>().unwrap(),
- "0:0:0:8::/61".parse::<Ipv6Cidr>().unwrap(),
- "0:0:0:10::/60".parse::<Ipv6Cidr>().unwrap(),
- "0:0:0:20::/59".parse::<Ipv6Cidr>().unwrap(),
- "0:0:0:40::/58".parse::<Ipv6Cidr>().unwrap(),
- "0:0:0:80::/57".parse::<Ipv6Cidr>().unwrap(),
- "0:0:0:100::/56".parse::<Ipv6Cidr>().unwrap(),
- "0:0:0:200::/55".parse::<Ipv6Cidr>().unwrap(),
- "0:0:0:400::/54".parse::<Ipv6Cidr>().unwrap(),
- "0:0:0:800::/53".parse::<Ipv6Cidr>().unwrap(),
- "0:0:0:1000::/52".parse::<Ipv6Cidr>().unwrap(),
- "0:0:0:2000::/51".parse::<Ipv6Cidr>().unwrap(),
- "0:0:0:4000::/50".parse::<Ipv6Cidr>().unwrap(),
- "0:0:0:8000::/49".parse::<Ipv6Cidr>().unwrap(),
- "0:0:1::/48".parse::<Ipv6Cidr>().unwrap(),
- "0:0:2::/47".parse::<Ipv6Cidr>().unwrap(),
- "0:0:4::/46".parse::<Ipv6Cidr>().unwrap(),
- "0:0:8::/45".parse::<Ipv6Cidr>().unwrap(),
- "0:0:10::/44".parse::<Ipv6Cidr>().unwrap(),
- "0:0:20::/43".parse::<Ipv6Cidr>().unwrap(),
- "0:0:40::/42".parse::<Ipv6Cidr>().unwrap(),
- "0:0:80::/41".parse::<Ipv6Cidr>().unwrap(),
- "0:0:100::/40".parse::<Ipv6Cidr>().unwrap(),
- "0:0:200::/39".parse::<Ipv6Cidr>().unwrap(),
- "0:0:400::/38".parse::<Ipv6Cidr>().unwrap(),
- "0:0:800::/37".parse::<Ipv6Cidr>().unwrap(),
- "0:0:1000::/36".parse::<Ipv6Cidr>().unwrap(),
- "0:0:2000::/35".parse::<Ipv6Cidr>().unwrap(),
- "0:0:4000::/34".parse::<Ipv6Cidr>().unwrap(),
- "0:0:8000::/33".parse::<Ipv6Cidr>().unwrap(),
- "0:1::/32".parse::<Ipv6Cidr>().unwrap(),
- "0:2::/31".parse::<Ipv6Cidr>().unwrap(),
- "0:4::/30".parse::<Ipv6Cidr>().unwrap(),
- "0:8::/29".parse::<Ipv6Cidr>().unwrap(),
- "0:10::/28".parse::<Ipv6Cidr>().unwrap(),
- "0:20::/27".parse::<Ipv6Cidr>().unwrap(),
- "0:40::/26".parse::<Ipv6Cidr>().unwrap(),
- "0:80::/25".parse::<Ipv6Cidr>().unwrap(),
- "0:100::/24".parse::<Ipv6Cidr>().unwrap(),
- "0:200::/23".parse::<Ipv6Cidr>().unwrap(),
- "0:400::/22".parse::<Ipv6Cidr>().unwrap(),
- "0:800::/21".parse::<Ipv6Cidr>().unwrap(),
- "0:1000::/20".parse::<Ipv6Cidr>().unwrap(),
- "0:2000::/19".parse::<Ipv6Cidr>().unwrap(),
- "0:4000::/18".parse::<Ipv6Cidr>().unwrap(),
- "0:8000::/17".parse::<Ipv6Cidr>().unwrap(),
- "1::/16".parse::<Ipv6Cidr>().unwrap(),
- "2::/15".parse::<Ipv6Cidr>().unwrap(),
- "4::/14".parse::<Ipv6Cidr>().unwrap(),
- "8::/13".parse::<Ipv6Cidr>().unwrap(),
- "10::/12".parse::<Ipv6Cidr>().unwrap(),
- "20::/11".parse::<Ipv6Cidr>().unwrap(),
- "40::/10".parse::<Ipv6Cidr>().unwrap(),
- "80::/9".parse::<Ipv6Cidr>().unwrap(),
- "100::/8".parse::<Ipv6Cidr>().unwrap(),
- "200::/7".parse::<Ipv6Cidr>().unwrap(),
- "400::/6".parse::<Ipv6Cidr>().unwrap(),
- "800::/5".parse::<Ipv6Cidr>().unwrap(),
- "1000::/4".parse::<Ipv6Cidr>().unwrap(),
- "2000::/3".parse::<Ipv6Cidr>().unwrap(),
- "4000::/2".parse::<Ipv6Cidr>().unwrap(),
- "8000::/1".parse::<Ipv6Cidr>().unwrap(),
- ],
- range.to_cidrs().as_slice()
- );
-
- let range = AddressRange::new_v6(
- [0, 0, 0, 0, 0, 0, 0, 0],
- [
- 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFE,
- ],
- )
- .unwrap();
-
- assert_eq!(
- [
- "::/1".parse::<Ipv6Cidr>().unwrap(),
- "8000::/2".parse::<Ipv6Cidr>().unwrap(),
- "c000::/3".parse::<Ipv6Cidr>().unwrap(),
- "e000::/4".parse::<Ipv6Cidr>().unwrap(),
- "f000::/5".parse::<Ipv6Cidr>().unwrap(),
- "f800::/6".parse::<Ipv6Cidr>().unwrap(),
- "fc00::/7".parse::<Ipv6Cidr>().unwrap(),
- "fe00::/8".parse::<Ipv6Cidr>().unwrap(),
- "ff00::/9".parse::<Ipv6Cidr>().unwrap(),
- "ff80::/10".parse::<Ipv6Cidr>().unwrap(),
- "ffc0::/11".parse::<Ipv6Cidr>().unwrap(),
- "ffe0::/12".parse::<Ipv6Cidr>().unwrap(),
- "fff0::/13".parse::<Ipv6Cidr>().unwrap(),
- "fff8::/14".parse::<Ipv6Cidr>().unwrap(),
- "fffc::/15".parse::<Ipv6Cidr>().unwrap(),
- "fffe::/16".parse::<Ipv6Cidr>().unwrap(),
- "ffff::/17".parse::<Ipv6Cidr>().unwrap(),
- "ffff:8000::/18".parse::<Ipv6Cidr>().unwrap(),
- "ffff:c000::/19".parse::<Ipv6Cidr>().unwrap(),
- "ffff:e000::/20".parse::<Ipv6Cidr>().unwrap(),
- "ffff:f000::/21".parse::<Ipv6Cidr>().unwrap(),
- "ffff:f800::/22".parse::<Ipv6Cidr>().unwrap(),
- "ffff:fc00::/23".parse::<Ipv6Cidr>().unwrap(),
- "ffff:fe00::/24".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ff00::/25".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ff80::/26".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffc0::/27".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffe0::/28".parse::<Ipv6Cidr>().unwrap(),
- "ffff:fff0::/29".parse::<Ipv6Cidr>().unwrap(),
- "ffff:fff8::/30".parse::<Ipv6Cidr>().unwrap(),
- "ffff:fffc::/31".parse::<Ipv6Cidr>().unwrap(),
- "ffff:fffe::/32".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff::/33".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:8000::/34".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:c000::/35".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:e000::/36".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:f000::/37".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:f800::/38".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:fc00::/39".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:fe00::/40".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ff00::/41".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ff80::/42".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffc0::/43".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffe0::/44".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:fff0::/45".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:fff8::/46".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:fffc::/47".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:fffe::/48".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff::/49".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:8000::/50".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:c000::/51".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:e000::/52".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:f000::/53".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:f800::/54".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:fc00::/55".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:fe00::/56".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:ff00::/57".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:ff80::/58".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:ffc0::/59".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:ffe0::/60".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:fff0::/61".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:fff8::/62".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:fffc::/63".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:fffe::/64".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:ffff::/65".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:ffff:8000::/66".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:ffff:c000::/67".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:ffff:e000::/68".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:ffff:f000::/69".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:ffff:f800::/70".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:ffff:fc00::/71".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:ffff:fe00::/72".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:ffff:ff00::/73".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:ffff:ff80::/74".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:ffff:ffc0::/75".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:ffff:ffe0::/76".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:ffff:fff0::/77".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:ffff:fff8::/78".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:ffff:fffc::/79".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:ffff:fffe::/80".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:ffff:ffff::/81".parse::<Ipv6Cidr>().unwrap(),
- "ffff:ffff:ffff:ffff:ffff:8000::/82"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:c000::/83"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:e000::/84"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:f000::/85"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:f800::/86"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:fc00::/87"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:fe00::/88"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ff00::/89"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ff80::/90"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffc0::/91"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffe0::/92"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:fff0::/93"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:fff8::/94"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:fffc::/95"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:fffe::/96"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff::/97"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:8000:0/98"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:c000:0/99"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:e000:0/100"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:f000:0/101"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:f800:0/102"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:fc00:0/103"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:fe00:0/104"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:ff00:0/105"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:ff80:0/106"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:ffc0:0/107"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:ffe0:0/108"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:fff0:0/109"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:fff8:0/110"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:fffc:0/111"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:fffe:0/112"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:ffff:0/113"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:ffff:8000/114"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:ffff:c000/115"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:ffff:e000/116"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:ffff:f000/117"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:ffff:f800/118"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fc00/119"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fe00/120"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00/121"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff80/122"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffc0/123"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffe0/124"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fff0/125"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fff8/126"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffc/127"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe/128"
- .parse::<Ipv6Cidr>()
- .unwrap(),
- ],
- range.to_cidrs().as_slice()
- );
-
- let range =
- AddressRange::new_v6([0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0]).unwrap();
-
- assert_eq!(
- [Ipv6Cidr::new([0, 0, 0, 0, 0, 0, 0, 0], 128).unwrap(),],
- range.to_cidrs().as_slice()
- );
-
- let range = AddressRange::new_v6(
- [
- 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
- ],
- [
- 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
- ],
- )
- .unwrap();
-
- assert_eq!(
- [Ipv6Cidr::new(
- [0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF],
- 128
- )
- .unwrap(),],
- range.to_cidrs().as_slice()
- );
- }
}
diff --git a/proxmox-ve-config/src/firewall/types/alias.rs b/proxmox-ve-config/src/firewall/types/alias.rs
index 7bc2fb8..a463e52 100644
--- a/proxmox-ve-config/src/firewall/types/alias.rs
+++ b/proxmox-ve-config/src/firewall/types/alias.rs
@@ -2,10 +2,10 @@ use std::fmt::Display;
use std::str::FromStr;
use anyhow::{bail, format_err, Error};
+use proxmox_network_types::ip_address::Cidr;
use serde_with::{DeserializeFromStr, SerializeDisplay};
use crate::firewall::parse::{match_name, match_non_whitespace};
-use crate::firewall::types::address::Cidr;
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(Eq, PartialEq))]
diff --git a/proxmox-ve-config/src/firewall/types/ipset.rs b/proxmox-ve-config/src/firewall/types/ipset.rs
index fe5a930..2aaf261 100644
--- a/proxmox-ve-config/src/firewall/types/ipset.rs
+++ b/proxmox-ve-config/src/firewall/types/ipset.rs
@@ -3,10 +3,10 @@ use std::ops::{Deref, DerefMut};
use std::str::FromStr;
use anyhow::{bail, format_err, Error};
+use proxmox_network_types::ip_address::{Cidr, IpRange};
use serde_with::DeserializeFromStr;
use crate::firewall::parse::match_non_whitespace;
-use crate::firewall::types::address::{Cidr, IpRange};
use crate::firewall::types::alias::AliasName;
use crate::guest::vm::NetworkConfig;
@@ -112,9 +112,9 @@ impl FromStr for IpsetAddress {
}
}
-impl<T: Into<Cidr>> From<T> for IpsetAddress {
- fn from(cidr: T) -> Self {
- IpsetAddress::Cidr(cidr.into())
+impl From<Cidr> for IpsetAddress {
+ fn from(cidr: Cidr) -> Self {
+ IpsetAddress::Cidr(cidr)
}
}
diff --git a/proxmox-ve-config/src/firewall/types/mod.rs b/proxmox-ve-config/src/firewall/types/mod.rs
index 8fd551e..567b302 100644
--- a/proxmox-ve-config/src/firewall/types/mod.rs
+++ b/proxmox-ve-config/src/firewall/types/mod.rs
@@ -7,7 +7,6 @@ pub mod port;
pub mod rule;
pub mod rule_match;
-pub use address::Cidr;
pub use alias::Alias;
pub use group::Group;
pub use ipset::Ipset;
diff --git a/proxmox-ve-config/src/firewall/types/rule.rs b/proxmox-ve-config/src/firewall/types/rule.rs
index 2c8f49c..b6b83d2 100644
--- a/proxmox-ve-config/src/firewall/types/rule.rs
+++ b/proxmox-ve-config/src/firewall/types/rule.rs
@@ -247,13 +247,14 @@ impl FromStr for RuleGroup {
#[cfg(test)]
mod tests {
+ use proxmox_network_types::ip_address::{Cidr, IpRange};
+
use crate::firewall::types::{
- address::{IpEntry, IpList, IpRange},
+ address::{IpEntry, IpList},
alias::{AliasName, AliasScope},
ipset::{IpsetName, IpsetScope},
log::LogLevel,
rule_match::{Icmp, IcmpCode, IpAddrMatch, IpMatch, Ports, Protocol, Udp},
- Cidr,
};
use super::*;
diff --git a/proxmox-ve-config/src/firewall/types/rule_match.rs b/proxmox-ve-config/src/firewall/types/rule_match.rs
index 94d8624..3256497 100644
--- a/proxmox-ve-config/src/firewall/types/rule_match.rs
+++ b/proxmox-ve-config/src/firewall/types/rule_match.rs
@@ -7,10 +7,11 @@ use serde::Deserialize;
use anyhow::{bail, format_err, Error};
use serde::de::IntoDeserializer;
+use proxmox_network_types::ip_address::Family;
use proxmox_sortable_macro::sortable;
use crate::firewall::parse::{match_name, match_non_whitespace, SomeStr};
-use crate::firewall::types::address::{Family, IpList};
+use crate::firewall::types::address::IpList;
use crate::firewall::types::alias::AliasName;
use crate::firewall::types::ipset::IpsetName;
use crate::firewall::types::log::LogLevel;
@@ -770,7 +771,8 @@ impl fmt::Display for Icmpv6Code {
#[cfg(test)]
mod tests {
- use crate::firewall::types::{alias::AliasScope::Guest, Cidr};
+ use proxmox_network_types::ip_address::Cidr;
+ use crate::firewall::types::alias::AliasScope::Guest;
use super::*;
diff --git a/proxmox-ve-config/src/guest/vm.rs b/proxmox-ve-config/src/guest/vm.rs
index d656a61..34b9075 100644
--- a/proxmox-ve-config/src/guest/vm.rs
+++ b/proxmox-ve-config/src/guest/vm.rs
@@ -1,82 +1,18 @@
-use core::fmt::Display;
+use std::collections::HashMap;
use std::io;
use std::str::FromStr;
-use std::{collections::HashMap, net::Ipv6Addr};
-
-use proxmox_schema::property_string::PropertyString;
-use proxmox_sortable_macro::sortable;
use anyhow::{bail, Error};
-use proxmox_schema::{ApiType, BooleanSchema, KeyAliasInfo, ObjectSchema, StringSchema};
use serde::Deserialize;
use serde_with::DeserializeFromStr;
-use crate::firewall::parse::match_digits;
-use crate::firewall::types::address::{Ipv4Cidr, Ipv6Cidr};
-
-#[derive(Clone, Copy, Debug, DeserializeFromStr, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub struct MacAddress([u8; 6]);
-
-static LOCAL_PART: [u8; 8] = [0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
-static EUI64_MIDDLE_PART: [u8; 2] = [0xFF, 0xFE];
-
-impl MacAddress {
- pub fn new(address: [u8; 6]) -> Self {
- Self(address)
- }
-
- /// generates a link local IPv6-address according to RFC 4291 (Appendix A)
- pub fn eui64_link_local_address(&self) -> Ipv6Addr {
- let head = &self.0[..3];
- let tail = &self.0[3..];
-
- let mut eui64_address: Vec<u8> = LOCAL_PART
- .iter()
- .chain(head.iter())
- .chain(EUI64_MIDDLE_PART.iter())
- .chain(tail.iter())
- .copied()
- .collect();
-
- // we need to flip the 7th bit of the first eui64 byte
- eui64_address[8] ^= 0x02;
-
- Ipv6Addr::from(
- TryInto::<[u8; 16]>::try_into(eui64_address).expect("is an u8 array with 16 entries"),
- )
- }
-}
-
-impl FromStr for MacAddress {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- let split = s.split(':');
-
- let parsed = split
- .into_iter()
- .map(|elem| u8::from_str_radix(elem, 16))
- .collect::<Result<Vec<u8>, _>>()
- .map_err(Error::msg)?;
-
- if parsed.len() != 6 {
- bail!("Invalid amount of elements in MAC address!");
- }
-
- let address = &parsed.as_slice()[0..6];
- Ok(Self(address.try_into().unwrap()))
- }
-}
+use proxmox_network_types::ip_address::{Ipv4Cidr, Ipv6Cidr};
+use proxmox_network_types::mac_address::MacAddress;
+use proxmox_schema::property_string::PropertyString;
+use proxmox_schema::{ApiType, BooleanSchema, KeyAliasInfo, ObjectSchema, StringSchema};
+use proxmox_sortable_macro::sortable;
-impl Display for MacAddress {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(
- f,
- "{:<02X}:{:<02X}:{:<02X}:{:<02X}:{:<02X}:{:<02X}",
- self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5]
- )
- }
-}
+use crate::firewall::parse::match_digits;
/// All possible models of network devices for both QEMU and LXC guests.
#[derive(Debug, Clone, Copy, DeserializeFromStr)]
@@ -410,6 +346,8 @@ impl NetworkConfig {
#[cfg(test)]
mod tests {
+ use std::net::Ipv6Addr;
+
use super::*;
#[test]
@@ -458,7 +396,7 @@ mod tests {
network_device,
NetworkDevice::Qemu(QemuNetworkDevice {
model: NetworkDeviceModel::VirtIO,
- mac_address: MacAddress([0xAA, 0xAA, 0xAA, 0x17, 0x19, 0x81]),
+ mac_address: MacAddress::new([0xAA, 0xAA, 0xAA, 0x17, 0x19, 0x81]),
firewall: Some(true),
})
);
@@ -471,7 +409,7 @@ mod tests {
network_device,
NetworkDevice::Qemu(QemuNetworkDevice {
model: NetworkDeviceModel::VirtIO,
- mac_address: MacAddress([0xAA, 0xAA, 0xAA, 0x17, 0x19, 0x81]),
+ mac_address: MacAddress::new([0xAA, 0xAA, 0xAA, 0x17, 0x19, 0x81]),
firewall: None,
})
);
@@ -486,7 +424,7 @@ mod tests {
network_device,
NetworkDevice::Qemu(QemuNetworkDevice {
model: NetworkDeviceModel::VirtIO,
- mac_address: MacAddress([0xAA, 0xAA, 0xAA, 0x17, 0x19, 0x81]),
+ mac_address: MacAddress::new([0xAA, 0xAA, 0xAA, 0x17, 0x19, 0x81]),
firewall: Some(true),
})
);
@@ -502,7 +440,7 @@ mod tests {
network_device,
NetworkDevice::Lxc(LxcNetworkDevice {
ty: NetworkDeviceModel::Veth,
- mac_address: MacAddress([0xAA, 0xAA, 0xAA, 0xE2, 0x3E, 0x24]),
+ mac_address: MacAddress::new([0xAA, 0xAA, 0xAA, 0xE2, 0x3E, 0x24]),
firewall: Some(false),
ip: Some(LxcIpv4Addr::Dhcp),
ip6: None,
@@ -592,7 +530,7 @@ vmgenid: 706fbe99-d28b-4047-a9cd-3677c859ca8a"
network_config.network_devices()[&0],
NetworkDevice::Qemu(QemuNetworkDevice {
model: NetworkDeviceModel::VirtIO,
- mac_address: MacAddress([0xAA, 0xBB, 0xCC, 0xF2, 0xFE, 0x75]),
+ mac_address: MacAddress::new([0xAA, 0xBB, 0xCC, 0xF2, 0xFE, 0x75]),
firewall: None,
})
);
@@ -620,7 +558,7 @@ unprivileged: 1"
network_config.network_devices()[&0],
NetworkDevice::Lxc(LxcNetworkDevice {
ty: NetworkDeviceModel::Veth,
- mac_address: MacAddress([0xBC, 0x24, 0x11, 0x47, 0x83, 0x11]),
+ mac_address: MacAddress::new([0xBC, 0x24, 0x11, 0x47, 0x83, 0x11]),
firewall: Some(true),
ip: Some(LxcIpv4Addr::Dhcp),
ip6: Some(LxcIpv6Addr::Auto),
@@ -631,7 +569,7 @@ unprivileged: 1"
network_config.network_devices()[&2],
NetworkDevice::Lxc(LxcNetworkDevice {
ty: NetworkDeviceModel::Veth,
- mac_address: MacAddress([0xBC, 0x24, 0x11, 0x47, 0x83, 0x12]),
+ mac_address: MacAddress::new([0xBC, 0x24, 0x11, 0x47, 0x83, 0x12]),
firewall: Some(false),
ip: Some(LxcIpv4Addr::Ip(
Ipv4Cidr::from_str("123.123.123.123/24").expect("valid ipv4")
@@ -644,7 +582,7 @@ unprivileged: 1"
network_config.network_devices()[&5],
NetworkDevice::Lxc(LxcNetworkDevice {
ty: NetworkDeviceModel::Veth,
- mac_address: MacAddress([0xBC, 0x24, 0x11, 0x47, 0x83, 0x13]),
+ mac_address: MacAddress::new([0xBC, 0x24, 0x11, 0x47, 0x83, 0x13]),
firewall: Some(true),
ip: None,
ip6: Some(LxcIpv6Addr::Ip(
diff --git a/proxmox-ve-config/src/host/utils.rs b/proxmox-ve-config/src/host/utils.rs
index b1dc8e9..3dee72b 100644
--- a/proxmox-ve-config/src/host/utils.rs
+++ b/proxmox-ve-config/src/host/utils.rs
@@ -1,6 +1,6 @@
use std::net::{IpAddr, ToSocketAddrs};
-use crate::firewall::types::Cidr;
+use proxmox_network_types::ip_address::Cidr;
use nix::sys::socket::{AddressFamily, SockaddrLike};
use proxmox_sys::nodename;
diff --git a/proxmox-ve-config/src/sdn/config.rs b/proxmox-ve-config/src/sdn/config.rs
index 880efc2..898121c 100644
--- a/proxmox-ve-config/src/sdn/config.rs
+++ b/proxmox-ve-config/src/sdn/config.rs
@@ -6,17 +6,16 @@ use std::{
str::FromStr,
};
+use proxmox_network_types::ip_address::{Cidr, IpRange, IpRangeError};
use proxmox_schema::{property_string::PropertyString, ApiType, ObjectSchema, StringSchema};
-
use serde::Deserialize;
use serde_with::{DeserializeFromStr, SerializeDisplay};
use crate::{
common::Allowlist,
firewall::types::{
- address::{IpRange, IpRangeError},
ipset::{IpsetEntry, IpsetName, IpsetScope},
- Cidr, Ipset,
+ Ipset,
},
sdn::{SdnNameError, SubnetName, VnetName, ZoneName},
};
@@ -587,10 +586,10 @@ impl SdnConfig {
ipset_all_wo_gateway.push((*subnet.cidr()).into());
if let Some(gateway) = subnet.gateway {
- let gateway_nomatch = IpsetEntry::new(gateway, true, None);
+ let gateway_nomatch = IpsetEntry::new(Cidr::from(gateway), true, None);
ipset_all_wo_gateway.push(gateway_nomatch);
- ipset_gateway.push(gateway.into());
+ ipset_gateway.push(Cidr::from(gateway).into());
}
ipset_dhcp.extend(subnet.dhcp_range.iter().cloned().map(IpsetEntry::from));
diff --git a/proxmox-ve-config/src/sdn/ipam.rs b/proxmox-ve-config/src/sdn/ipam.rs
index 598b835..9c6985b 100644
--- a/proxmox-ve-config/src/sdn/ipam.rs
+++ b/proxmox-ve-config/src/sdn/ipam.rs
@@ -7,13 +7,16 @@ use std::{
use serde::Deserialize;
+use proxmox_network_types::ip_address::Cidr;
+use proxmox_network_types::mac_address::MacAddress;
+
use crate::{
common::Allowlist,
firewall::types::{
- ipset::{IpsetEntry, IpsetScope},
- Cidr, Ipset,
+ ipset::IpsetScope,
+ Ipset,
},
- guest::{types::Vmid, vm::MacAddress},
+ guest::types::Vmid,
sdn::{SdnNameError, SubnetName, ZoneName},
};
@@ -339,7 +342,7 @@ impl Ipam {
.or_insert_with(|| {
Ipset::from_parts(IpsetScope::Sdn, format!("guest-ipam-{}", entry.vmid))
})
- .push(IpsetEntry::from(entry.ip));
+ .push(Cidr::from(entry.ip).into());
acc
})
diff --git a/proxmox-ve-config/src/sdn/mod.rs b/proxmox-ve-config/src/sdn/mod.rs
index c8dc724..cde6fed 100644
--- a/proxmox-ve-config/src/sdn/mod.rs
+++ b/proxmox-ve-config/src/sdn/mod.rs
@@ -3,10 +3,9 @@ pub mod ipam;
use std::{error::Error, fmt::Display, str::FromStr};
+use proxmox_network_types::ip_address::Cidr;
use serde_with::DeserializeFromStr;
-use crate::firewall::types::Cidr;
-
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum SdnNameError {
Empty,
diff --git a/proxmox-ve-config/tests/sdn/main.rs b/proxmox-ve-config/tests/sdn/main.rs
index 1815bec..2ab0e5c 100644
--- a/proxmox-ve-config/tests/sdn/main.rs
+++ b/proxmox-ve-config/tests/sdn/main.rs
@@ -3,18 +3,17 @@ use std::{
str::FromStr,
};
-use proxmox_ve_config::{
- firewall::types::{address::IpRange, Cidr},
- guest::vm::MacAddress,
- sdn::{
+use proxmox_network_types::ip_address::{Cidr, IpRange};
+use proxmox_network_types::mac_address::MacAddress;
+
+use proxmox_ve_config::sdn::{
config::{
RunningConfig, SdnConfig, SdnConfigError, SubnetConfig, VnetConfig, ZoneConfig,
ZoneType,
},
ipam::{Ipam, IpamDataVm, IpamEntry, IpamJson},
SubnetName, VnetName, ZoneName,
- },
-};
+ };
#[test]
fn parse_running_config() {
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs v3 03/21] sdn-types: initial commit
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (6 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 02/21] ve-config: move types to proxmox-network-types Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 04/21] frr: create proxmox-frr crate Stefan Hanreich
` (66 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
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)
* OSPF Area
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
Cargo.toml | 9 +
proxmox-sdn-types/Cargo.toml | 19 ++
proxmox-sdn-types/debian/changelog | 5 +
proxmox-sdn-types/debian/control | 53 ++++
proxmox-sdn-types/debian/copyright | 18 ++
proxmox-sdn-types/debian/debcargo.toml | 7 +
proxmox-sdn-types/src/area.rs | 50 ++++
proxmox-sdn-types/src/lib.rs | 3 +
proxmox-sdn-types/src/net.rs | 329 +++++++++++++++++++++++++
proxmox-sdn-types/src/openfabric.rs | 72 ++++++
proxmox-ve-config/Cargo.toml | 8 +-
11 files changed, 569 insertions(+), 4 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/area.rs
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 b6e6df7..4f5a6ca 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,7 @@
[workspace]
members = [
"proxmox-ve-config",
+ "proxmox-sdn-types",
]
exclude = [
"build",
@@ -16,4 +17,12 @@ exclude = [ "debian" ]
rust-version = "1.82"
[workspace.dependencies]
+anyhow = "1"
+const_format = "0.2"
+regex = "1.7"
+serde = { version = "1" }
+serde_with = "3"
+thiserror = "1.0.59"
+
proxmox-network-types = { version = "0.1" }
+proxmox-schema = { version = "4" }
diff --git a/proxmox-sdn-types/Cargo.toml b/proxmox-sdn-types/Cargo.toml
new file mode 100644
index 0000000..09cd581
--- /dev/null
+++ b/proxmox-sdn-types/Cargo.toml
@@ -0,0 +1,19 @@
+[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]
+anyhow = { workspace = true }
+const_format = { workspace = true }
+regex = { workspace = true }
+serde = { workspace = true, features = [ "derive" ] }
+serde_with = { workspace = true }
+
+proxmox-schema = { workspace = true, features = [ "api-macro", "api-types" ] }
+proxmox-serde = { version = "0.1.2", features = [ "perl" ] }
diff --git a/proxmox-sdn-types/debian/changelog b/proxmox-sdn-types/debian/changelog
new file mode 100644
index 0000000..422921c
--- /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@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 0000000..bfdb47e
--- /dev/null
+++ b/proxmox-sdn-types/debian/control
@@ -0,0 +1,53 @@
+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-anyhow-1+default-dev <!nocheck>,
+ librust-const-format-0.2+default-dev <!nocheck>,
+ librust-proxmox-schema-4+api-macro-dev <!nocheck>,
+ librust-proxmox-schema-4+api-types-dev <!nocheck>,
+ librust-proxmox-schema-4+default-dev <!nocheck>,
+ librust-proxmox-serde-0.1+default-dev (>= 0.1.2-~~) <!nocheck>,
+ librust-proxmox-serde-0.1+perl-dev (>= 0.1.2-~~) <!nocheck>,
+ librust-regex-1+default-dev (>= 1.7-~~) <!nocheck>,
+ librust-serde-1+default-dev <!nocheck>,
+ librust-serde-1+derive-dev <!nocheck>,
+ librust-serde-with-3+default-dev <!nocheck>
+Maintainer: Proxmox Support Team <support@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-anyhow-1+default-dev,
+ librust-const-format-0.2+default-dev,
+ librust-proxmox-schema-4+api-macro-dev,
+ librust-proxmox-schema-4+api-types-dev,
+ librust-proxmox-schema-4+default-dev,
+ librust-proxmox-serde-0.1+default-dev (>= 0.1.2-~~),
+ librust-proxmox-serde-0.1+perl-dev (>= 0.1.2-~~),
+ librust-regex-1+default-dev (>= 1.7-~~),
+ librust-serde-1+default-dev,
+ librust-serde-1+derive-dev,
+ librust-serde-with-3+default-dev
+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 0000000..1ea8a56
--- /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@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 0000000..87a787e
--- /dev/null
+++ b/proxmox-sdn-types/debian/debcargo.toml
@@ -0,0 +1,7 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@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/area.rs b/proxmox-sdn-types/src/area.rs
new file mode 100644
index 0000000..067e9f6
--- /dev/null
+++ b/proxmox-sdn-types/src/area.rs
@@ -0,0 +1,50 @@
+use std::{fmt::Display, net::Ipv4Addr};
+
+use anyhow::Error;
+use proxmox_schema::{ApiType, Schema, StringSchema, UpdaterType};
+use serde_with::{DeserializeFromStr, SerializeDisplay};
+
+/// An OSPF Area.
+///
+/// Internally the area is just a 32 bit number and is often represented in dotted-decimal
+/// notation, like an IPv4. FRR also allows us to specify it as a number or an IPv4-Address.
+/// To keep a nice user experience we keep whichever format the user entered.
+#[derive(
+ Debug, DeserializeFromStr, SerializeDisplay, Clone, Hash, PartialEq, Eq, PartialOrd, Ord,
+)]
+pub enum Area {
+ Number(u32),
+ IpAddress(Ipv4Addr),
+}
+
+impl ApiType for Area {
+ const API_SCHEMA: Schema =
+ StringSchema::new("The OSPF area, which can be a number or a ip-address.").schema();
+}
+
+impl UpdaterType for Area {
+ type Updater = Option<Area>;
+}
+
+impl std::str::FromStr for Area {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ if let Ok(ip) = Ipv4Addr::from_str(s) {
+ Ok(Self::IpAddress(ip))
+ } else if let Ok(number) = u32::from_str(s) {
+ Ok(Self::Number(number))
+ } else {
+ anyhow::bail!("Area is not a number, nor an ip address");
+ }
+ }
+}
+
+impl Display for Area {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Area::Number(n) => write!(f, "{n}"),
+ Area::IpAddress(i) => write!(f, "{i}"),
+ }
+ }
+}
diff --git a/proxmox-sdn-types/src/lib.rs b/proxmox-sdn-types/src/lib.rs
new file mode 100644
index 0000000..f582d90
--- /dev/null
+++ b/proxmox-sdn-types/src/lib.rs
@@ -0,0 +1,3 @@
+pub mod net;
+pub mod area;
+pub mod openfabric;
diff --git a/proxmox-sdn-types/src/net.rs b/proxmox-sdn-types/src/net.rs
new file mode 100644
index 0000000..78a4798
--- /dev/null
+++ b/proxmox-sdn-types/src/net.rs
@@ -0,0 +1,329 @@
+use std::{
+ fmt::Display,
+ net::{IpAddr, Ipv4Addr, Ipv6Addr},
+};
+
+use anyhow::{bail, Error};
+use const_format::concatcp;
+use serde::{Deserialize, Serialize};
+
+use proxmox_schema::{api, api_string_type, const_regex, ApiStringFormat, UpdaterType};
+
+const NET_AFI_REGEX_STR: &str = r"(?:[a-fA-F0-9]{2})";
+const NET_AREA_REGEX_STR: &str = r"(?:[a-fA-F0-9]{4})";
+const NET_SYSTEM_ID_REGEX_STR: &str = r"(?:[a-fA-F0-9]{4})\.(?:[a-fA-F0-9]{4})\.(?:[a-fA-F0-9]{4})";
+const NET_SELECTOR_REGEX_STR: &str = r"(?:[a-fA-F0-9]{2})";
+
+const_regex! {
+ NET_AFI_REGEX = concatcp!(r"^", NET_AFI_REGEX_STR, r"$");
+ NET_AREA_REGEX = concatcp!(r"^", NET_AREA_REGEX_STR, r"$");
+ NET_SYSTEM_ID_REGEX = concatcp!(r"^", NET_SYSTEM_ID_REGEX_STR, r"$");
+ NET_SELECTOR_REGEX = concatcp!(r"^", NET_SELECTOR_REGEX_STR, r"$");
+}
+
+const NET_AFI_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&NET_AFI_REGEX);
+const NET_AREA_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&NET_AREA_REGEX);
+const NET_SYSTEM_ID_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&NET_SYSTEM_ID_REGEX);
+const NET_SELECTOR_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&NET_SELECTOR_REGEX);
+
+api_string_type! {
+ /// Address Family authority Identifier - 49 The AFI value 49 is what IS-IS (and openfabric) uses
+ /// for private addressing.
+ #[api(format: &NET_AFI_FORMAT)]
+ #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
+ struct NetAFI(String);
+}
+
+impl Default for NetAFI {
+ fn default() -> Self {
+ Self("49".to_owned())
+ }
+}
+
+impl UpdaterType for NetAFI {
+ type Updater = Option<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.
+ #[api(format: &NET_AREA_FORMAT)]
+ #[derive(Debug, Deserialize, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
+ struct NetArea(String);
+}
+
+impl Default for NetArea {
+ fn default() -> Self {
+ Self("0001".to_owned())
+ }
+}
+
+impl UpdaterType for NetArea {
+ type Updater = Option<NetArea>;
+}
+
+api_string_type! {
+ /// 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.
+ #[api(format: &NET_SYSTEM_ID_FORMAT)]
+ #[derive(Debug, Deserialize, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
+ struct NetSystemId(String);
+}
+
+impl UpdaterType for NetSystemId {
+ type Updater = Option<NetSystemId>;
+}
+
+/// 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)
+ }
+}
+
+api_string_type! {
+ /// NET selector: 00 Must always be 00. This setting indicates “this system” or “local system.”
+ #[api(format: &NET_SELECTOR_FORMAT)]
+ #[derive(Debug, Deserialize, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
+ struct NetSelector(String);
+}
+
+impl UpdaterType for NetSelector {
+ type Updater = Option<NetSelector>;
+}
+
+impl Default for NetSelector {
+ fn default() -> Self {
+ Self("00".to_owned())
+ }
+}
+
+/// The Network Entity Title (NET).
+///
+/// Every OpenFabric node 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 (afi + 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, but *not*
+/// between fabrics on the same node. It contains the [`NetSystemId`] and the [`NetSelector`].
+/// e.g.: "1921.6800.1002.00"
+#[api]
+#[derive(Debug, Deserialize, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
+pub struct Net {
+ afi: NetAFI,
+ area: NetArea,
+ system: NetSystemId,
+ selector: NetSelector,
+}
+
+impl UpdaterType for Net {
+ type Updater = Option<Net>;
+}
+
+impl std::str::FromStr for Net {
+ type Err = Error;
+
+ 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}")
+ }
+
+ let system = format!("{}.{}.{}", parts[2], parts[3], parts[4],);
+
+ 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())?,
+ })
+ }
+}
+
+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
+ )
+ }
+}
+
+/// 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 {
+ match value {
+ IpAddr::V4(ipv4_addr) => ipv4_addr.into(),
+ IpAddr::V6(ipv6_addr) => ipv6_addr.into(),
+ }
+ }
+}
+
+#[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";
+ input.parse::<Net>().expect_err("invalid NET selector");
+
+ let input = "49.0001.1921.6800.1002.00.00";
+ input
+ .parse::<Net>()
+ .expect_err("invalid amount of elements");
+
+ let input = "49.0001.1921.6800.10002.00";
+ input.parse::<Net>().expect_err("invalid system id");
+
+ let input = "49.0001.1921.6800.1z02.00";
+ input.parse::<Net>().expect_err("invalid system id");
+
+ 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");
+ }
+
+ #[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");
+
+ // a:b::0 -> [a, b, 0, 0, 0, 0, 0, 0]
+ // last 3 segments: [0, 0, 0]
+ let ip4: Ipv6Addr = "a:b::0".parse().unwrap();
+ let net4: Net = ip4.into();
+ assert_eq!(format!("{net4}"), "49.0001.0000.0000.0000.00");
+ }
+}
diff --git a/proxmox-sdn-types/src/openfabric.rs b/proxmox-sdn-types/src/openfabric.rs
new file mode 100644
index 0000000..c79e2d9
--- /dev/null
+++ b/proxmox-sdn-types/src/openfabric.rs
@@ -0,0 +1,72 @@
+use serde::{Deserialize, Serialize};
+use std::fmt::Display;
+
+use proxmox_schema::{api, UpdaterType};
+
+/// The OpenFabric CSNP Interval.
+///
+/// The Complete Sequence Number Packets (CSNP) interval in seconds. The interval range is 1 to
+/// 600.
+#[api(
+ type: Integer,
+ minimum: 1,
+ maximum: 600,
+)]
+#[derive(Serialize, Deserialize, Hash, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+#[serde(transparent)]
+pub struct CsnpInterval(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u16")] u16);
+
+impl UpdaterType for CsnpInterval {
+ type Updater = Option<CsnpInterval>;
+}
+
+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.
+#[api(
+ type: Integer,
+ minimum: 1,
+ maximum: 600,
+)]
+#[derive(Serialize, Deserialize, Hash, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+#[serde(transparent)]
+pub struct HelloInterval(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u16")] u16);
+
+impl UpdaterType for HelloInterval {
+ type Updater = Option<HelloInterval>;
+}
+
+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.
+#[api(
+ type: Integer,
+ minimum: 2,
+ maximum: 100,
+)]
+#[derive(Serialize, Deserialize, Hash, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+#[serde(transparent)]
+pub struct HelloMultiplier(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u16")] u16);
+
+impl UpdaterType for HelloMultiplier {
+ type Updater = Option<HelloMultiplier>;
+}
+
+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 72fb627..c240a87 100644
--- a/proxmox-ve-config/Cargo.toml
+++ b/proxmox-ve-config/Cargo.toml
@@ -8,14 +8,14 @@ exclude.workspace = true
[dependencies]
log = "0.4"
-anyhow = "1"
+anyhow = { workspace = true }
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-serde = { version = "0.1.2", features = [ "perl" ]}
proxmox-network-types = { workspace = true }
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs v3 04/21] frr: create proxmox-frr crate
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (7 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 03/21] sdn-types: initial commit Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 05/21] frr: add common frr types Stefan Hanreich
` (65 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
This crate holds FRR-types, so rust-types that closely resemble
FRR-configuration items. These types can then simply be converted to
strings (and the final FRR config) by serializing. This has minimal
dependencies and it's only internal dependency is proxmox-network-types,
which holds common types. This way we could reuse proxmox-frr on
different products, without dragging product-specific types with us.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
Cargo.toml | 2 ++
proxmox-frr/Cargo.toml | 23 ++++++++++++++++
proxmox-frr/debian/changelog | 5 ++++
proxmox-frr/debian/control | 47 ++++++++++++++++++++++++++++++++
proxmox-frr/debian/copyright | 18 ++++++++++++
proxmox-frr/debian/debcargo.toml | 7 +++++
proxmox-frr/src/lib.rs | 0
7 files changed, 102 insertions(+)
create mode 100644 proxmox-frr/Cargo.toml
create mode 100644 proxmox-frr/debian/changelog
create mode 100644 proxmox-frr/debian/control
create mode 100644 proxmox-frr/debian/copyright
create mode 100644 proxmox-frr/debian/debcargo.toml
create mode 100644 proxmox-frr/src/lib.rs
diff --git a/Cargo.toml b/Cargo.toml
index 4f5a6ca..e51fb59 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,7 @@
[workspace]
members = [
"proxmox-ve-config",
+ "proxmox-frr",
"proxmox-sdn-types",
]
exclude = [
@@ -26,3 +27,4 @@ thiserror = "1.0.59"
proxmox-network-types = { version = "0.1" }
proxmox-schema = { version = "4" }
+proxmox-sdn-types = { version = "0.1", path = "proxmox-sdn-types" }
diff --git a/proxmox-frr/Cargo.toml b/proxmox-frr/Cargo.toml
new file mode 100644
index 0000000..e29453a
--- /dev/null
+++ b/proxmox-frr/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "proxmox-frr"
+description = "Rust types for the FRR configuration file"
+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-network-types = { workspace = true }
+proxmox-sdn-types = { workspace = true }
+
diff --git a/proxmox-frr/debian/changelog b/proxmox-frr/debian/changelog
new file mode 100644
index 0000000..47d7348
--- /dev/null
+++ b/proxmox-frr/debian/changelog
@@ -0,0 +1,5 @@
+rust-proxmox-frr (0.1.0-1) unstable; urgency=medium
+
+ * Initial release.
+
+ -- Proxmox Support Team <support@proxmox.com> Mon, 03 Jun 2024 10:51:11 +0200
diff --git a/proxmox-frr/debian/control b/proxmox-frr/debian/control
new file mode 100644
index 0000000..5afaf5b
--- /dev/null
+++ b/proxmox-frr/debian/control
@@ -0,0 +1,47 @@
+Source: rust-proxmox-frr
+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-anyhow-1+default-dev <!nocheck>,
+ librust-itoa-1+default-dev (>= 1.0.9-~~) <!nocheck>,
+ librust-proxmox-network-types-0.1+default-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>,
+ librust-tracing-0.1+default-dev <!nocheck>
+Maintainer: Proxmox Support Team <support@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-frr
+Rules-Requires-Root: no
+
+Package: librust-proxmox-frr-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-anyhow-1+default-dev,
+ librust-itoa-1+default-dev (>= 1.0.9-~~),
+ librust-proxmox-network-types-0.1+default-dev,
+ librust-serde-1+default-dev,
+ librust-serde-1+derive-dev,
+ librust-serde-with-3+default-dev,
+ librust-thiserror-1+default-dev (>= 1.0.59-~~),
+ librust-tracing-0.1+default-dev
+Provides:
+ librust-proxmox-frr+default-dev (= ${binary:Version}),
+ librust-proxmox-frr-0-dev (= ${binary:Version}),
+ librust-proxmox-frr-0+default-dev (= ${binary:Version}),
+ librust-proxmox-frr-0.1-dev (= ${binary:Version}),
+ librust-proxmox-frr-0.1+default-dev (= ${binary:Version}),
+ librust-proxmox-frr-0.1.0-dev (= ${binary:Version}),
+ librust-proxmox-frr-0.1.0+default-dev (= ${binary:Version})
+Description: Rust types for the FRR configuration file - Rust source code
+ Source code for Debianized Rust crate "proxmox-frr"
diff --git a/proxmox-frr/debian/copyright b/proxmox-frr/debian/copyright
new file mode 100644
index 0000000..1ea8a56
--- /dev/null
+++ b/proxmox-frr/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@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-frr/debian/debcargo.toml b/proxmox-frr/debian/debcargo.toml
new file mode 100644
index 0000000..87a787e
--- /dev/null
+++ b/proxmox-frr/debian/debcargo.toml
@@ -0,0 +1,7 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@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-frr/src/lib.rs b/proxmox-frr/src/lib.rs
new file mode 100644
index 0000000..e69de29
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs v3 05/21] frr: add common frr types
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (8 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 04/21] frr: create proxmox-frr crate Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 06/21] frr: add openfabric types Stefan Hanreich
` (64 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
Add common FRR configuration types such as FrrWord,
CommonInterfaceName, etc. These are some common types that are used by
both openfabric and ospf and the generic types that span the two
protocols. The FrrWord is a simple primitive in FRR, which is a
ascii-string that doesn't contain whitespaces.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
proxmox-frr/src/lib.rs | 118 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 118 insertions(+)
diff --git a/proxmox-frr/src/lib.rs b/proxmox-frr/src/lib.rs
index e69de29..5e0b346 100644
--- a/proxmox-frr/src/lib.rs
+++ b/proxmox-frr/src/lib.rs
@@ -0,0 +1,118 @@
+use std::{fmt::Display, str::FromStr};
+
+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),
+}
+
+/// 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, PartialOrd, Ord)]
+pub enum InterfaceName {
+ Openfabric(CommonInterfaceName),
+ Ospf(CommonInterfaceName),
+}
+
+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(Error, Debug)]
+pub enum FrrWordError {
+ #[error("word is empty")]
+ IsEmpty,
+ #[error("word contains invalid character")]
+ InvalidCharacter,
+}
+
+/// A simple FRR Word.
+///
+/// Every string argument or value in FRR is an FrrWord. FrrWords must only contain ascii
+/// characters and must not have a whitespace.
+#[derive(
+ Clone, Debug, PartialEq, Eq, Hash, DeserializeFromStr, SerializeDisplay, PartialOrd, Ord,
+)]
+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())
+ {
+ eprintln!("invalid char in: \"{name}\"");
+ 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
+ }
+}
+
+#[derive(Error, Debug)]
+pub enum CommonInterfaceNameError {
+ #[error("interface name too long")]
+ TooLong,
+}
+
+/// Name of a interface, which is common between all protocols.
+///
+/// FRR itself doesn't enforce any limits, but the kernel does. Linux only allows interface names
+/// to be a maximum of 16 bytes. This is enforced by this struct.
+#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash, PartialOrd, Ord)]
+pub struct CommonInterfaceName(String);
+
+impl FromStr for CommonInterfaceName {
+ type Err = CommonInterfaceNameError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ if s.len() <= 15 {
+ Ok(Self(s.to_owned()))
+ } else {
+ Err(CommonInterfaceNameError::TooLong)
+ }
+ }
+}
+
+impl Display for CommonInterfaceName {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs v3 06/21] frr: add openfabric types
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (9 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 05/21] frr: add common frr types Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 07/21] frr: add ospf types Stefan Hanreich
` (63 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
Implement OpenFabric-specific variants of common enums that
encapsulate protocol properties defined in proxmox-network-types. The
primary addition is OpenFabricInterface, which stores
protocol-specific timing parameters: HelloInterval (neighbor discovery
frequency), CsnpInterval (database synchronization frequency), and
HelloMultiplier (neighbor failure detection). Added `is_ipv6` flag to
support FRR's command prefixing requirements during serialization for
IPv6-specific commands (we need to add a 'ipv6' prefix to some
commands).
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
proxmox-frr/debian/control | 2 +
proxmox-frr/src/lib.rs | 1 +
proxmox-frr/src/openfabric.rs | 114 ++++++++++++++++++++++++++++++++++
3 files changed, 117 insertions(+)
create mode 100644 proxmox-frr/src/openfabric.rs
diff --git a/proxmox-frr/debian/control b/proxmox-frr/debian/control
index 5afaf5b..d5603ab 100644
--- a/proxmox-frr/debian/control
+++ b/proxmox-frr/debian/control
@@ -9,6 +9,7 @@ Build-Depends-Arch: cargo:native <!nocheck>,
librust-anyhow-1+default-dev <!nocheck>,
librust-itoa-1+default-dev (>= 1.0.9-~~) <!nocheck>,
librust-proxmox-network-types-0.1+default-dev <!nocheck>,
+ librust-proxmox-sdn-types-0.1+default-dev <!nocheck>,
librust-serde-1+default-dev <!nocheck>,
librust-serde-1+derive-dev <!nocheck>,
librust-serde-with-3+default-dev <!nocheck>,
@@ -30,6 +31,7 @@ Depends:
librust-anyhow-1+default-dev,
librust-itoa-1+default-dev (>= 1.0.9-~~),
librust-proxmox-network-types-0.1+default-dev,
+ librust-proxmox-sdn-types-0.1+default-dev,
librust-serde-1+default-dev,
librust-serde-1+derive-dev,
librust-serde-with-3+default-dev,
diff --git a/proxmox-frr/src/lib.rs b/proxmox-frr/src/lib.rs
index 5e0b346..ba9eedf 100644
--- a/proxmox-frr/src/lib.rs
+++ b/proxmox-frr/src/lib.rs
@@ -1,3 +1,4 @@
+pub mod openfabric;
use std::{fmt::Display, str::FromStr};
use serde::{Deserialize, Serialize};
diff --git a/proxmox-frr/src/openfabric.rs b/proxmox-frr/src/openfabric.rs
new file mode 100644
index 0000000..91c18dc
--- /dev/null
+++ b/proxmox-frr/src/openfabric.rs
@@ -0,0 +1,114 @@
+use std::fmt::Debug;
+use std::fmt::Display;
+
+use proxmox_sdn_types::net::Net;
+use serde::{Deserialize, Serialize};
+use serde_with::SerializeDisplay;
+
+use thiserror::Error;
+
+use crate::FrrWord;
+use crate::FrrWordError;
+
+/// The name of a OpenFabric router. Is an FrrWord.
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, SerializeDisplay, PartialOrd, Ord)]
+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 Display for OpenfabricRouterName {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "openfabric {}", self.0)
+ }
+}
+
+/// All the properties a OpenFabric router can hold.
+///
+/// These can serialized with a " " space prefix as they are in the `router openfabric` block.
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, PartialOrd, Ord)]
+pub struct OpenfabricRouter {
+ /// The NET address
+ pub net: Net,
+}
+
+impl OpenfabricRouter {
+ pub fn new(net: Net) -> Self {
+ Self { net }
+ }
+
+ pub fn net(&self) -> &Net {
+ &self.net
+ }
+}
+
+/// The OpenFabric properties.
+///
+/// This struct holds all the OpenFabric interface properties. The most important one here is the
+/// fabric_id, which ties the interface to a fabric. When serialized these properties all get
+/// prefixed with a space (" ") as they are inside the interface block. They serialize roughly to:
+///
+/// ```text
+/// interface ens20
+/// ip router openfabric <fabric_id>
+/// ipv6 router openfabric <fabric_id>
+/// openfabric hello-interval <value>
+/// openfabric hello-multiplier <value>
+/// openfabric csnp-interval <value>
+/// openfabric passive <value>
+/// ```
+///
+/// The is_ipv4 and is_ipv6 properties decide if we need to add `ip router openfabric`, `ipv6
+/// router openfabric`, or both. A interface can only be part of a single fabric.
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, PartialOrd, Ord)]
+pub struct OpenfabricInterface {
+ // Note: an interface can only be a part of a single fabric (so no vec needed here)
+ pub fabric_id: OpenfabricRouterName,
+ pub passive: Option<bool>,
+ pub hello_interval: Option<proxmox_sdn_types::openfabric::HelloInterval>,
+ pub csnp_interval: Option<proxmox_sdn_types::openfabric::CsnpInterval>,
+ pub hello_multiplier: Option<proxmox_sdn_types::openfabric::HelloMultiplier>,
+ pub is_ipv4: bool,
+ pub is_ipv6: bool,
+}
+
+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<proxmox_sdn_types::openfabric::HelloInterval> {
+ self.hello_interval
+ }
+ pub fn csnp_interval(&self) -> Option<proxmox_sdn_types::openfabric::CsnpInterval> {
+ self.csnp_interval
+ }
+ pub fn hello_multiplier(&self) -> Option<proxmox_sdn_types::openfabric::HelloMultiplier> {
+ self.hello_multiplier
+ }
+ pub fn set_hello_interval(
+ &mut self,
+ interval: impl Into<Option<proxmox_sdn_types::openfabric::HelloInterval>>,
+ ) {
+ self.hello_interval = interval.into();
+ }
+}
+
+#[derive(Error, Debug)]
+pub enum OpenfabricInterfaceError {
+ #[error("Unknown error converting to OpenFabricInterface")]
+ UnknownError,
+ #[error("Error parsing frr word")]
+ FrrWordParse(#[from] FrrWordError),
+}
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs v3 07/21] frr: add ospf types
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (10 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 06/21] frr: add openfabric types Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 08/21] frr: add route-map types Stefan Hanreich
` (62 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
Add OSPF-specific FRR types. This also reuses the types from
proxmox-network-types.
The NetworkType FRR option is implemented here, but not exposed to the
interface, as we want to keep it simple for the users. If they do not
set an IP, then the interface is considered to be unnumbered and uses
the Point-to-Point network type.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
proxmox-frr/src/lib.rs | 25 ++++++
proxmox-frr/src/ospf.rs | 179 ++++++++++++++++++++++++++++++++++++++++
2 files changed, 204 insertions(+)
create mode 100644 proxmox-frr/src/ospf.rs
diff --git a/proxmox-frr/src/lib.rs b/proxmox-frr/src/lib.rs
index ba9eedf..0d94aef 100644
--- a/proxmox-frr/src/lib.rs
+++ b/proxmox-frr/src/lib.rs
@@ -1,4 +1,5 @@
pub mod openfabric;
+pub mod ospf;
use std::{fmt::Display, str::FromStr};
use serde::{Deserialize, Serialize};
@@ -31,6 +32,30 @@ impl Display for InterfaceName {
}
}
+/// Generic FRR Interface.
+///
+/// In FRR config it looks like this:
+/// ```text
+/// interface <name>
+/// ! ...
+#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, PartialOrd, Ord)]
+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(Error, Debug)]
pub enum FrrWordError {
#[error("word is empty")]
diff --git a/proxmox-frr/src/ospf.rs b/proxmox-frr/src/ospf.rs
new file mode 100644
index 0000000..bebbd59
--- /dev/null
+++ b/proxmox-frr/src/ospf.rs
@@ -0,0 +1,179 @@
+use std::fmt::Debug;
+use std::fmt::Display;
+use std::net::Ipv4Addr;
+
+use serde::{Deserialize, Serialize};
+
+use thiserror::Error;
+
+use crate::{FrrWord, FrrWordError};
+
+/// The name of the ospf frr router.
+///
+/// We can only have a single ospf router (ignoring multiple invocations of the ospfd daemon)
+/// because the router-id needs to be the same between different routers on a single node.
+/// We can still have multiple fabrics by separating them using areas. Still, different areas have
+/// the same frr router, so the name of the router is just "ospf" in "router ospf".
+///
+/// This serializes roughly to:
+/// ```text
+/// router ospf
+/// !...
+/// ```
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, PartialOrd, Ord)]
+pub struct OspfRouterName;
+
+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.
+///
+/// The OSPF area is a pseud-ipaddress (so it looks like an ip-address but isn't set on any
+/// interface or even pingable), but can also be specified by a simple number. So you can use "5"
+/// or "0" as an area, which then gets translated to "0.0.0.5" and "0.0.0.0" by FRR. We allow both
+/// a number or an ip-address. Note that the area "0" (or "0.0.0.0") is a special area - it creates
+/// a OSPF "backbone" area.
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, PartialOrd, Ord)]
+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::<u32>().is_ok() || name.as_ref().parse::<Ipv4Addr>().is_ok() {
+ Ok(Self(name))
+ } else {
+ Err(AreaParsingError::InvalidArea)
+ }
+ }
+}
+
+impl Display for Area {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "area {}", self.0)
+ }
+}
+
+/// The OSPF router properties.
+///
+/// Currently the only property of a OSPF router is the router_id. The router_id is used to
+/// differentiate between nodes and every node in the same area must have a different router_id.
+/// The router_id must also be the same on the different fabrics on the same node. The OSPFv2
+/// daemon only supports IPv4.
+/// Note that these properties also serialize with a space prefix (" ") as they are inside the OSPF
+/// router block. It serializes roughly to:
+///
+/// ```text
+/// router ospf
+/// router-id <ipv4-address>
+/// ```
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, PartialOrd, Ord)]
+pub struct OspfRouter {
+ pub 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 OspfInterfaceError {
+ #[error("Error parsing area")]
+ AreaParsingError(#[from] AreaParsingError),
+ #[error("Error parsing frr word")]
+ FrrWordParse(#[from] FrrWordError),
+}
+
+/// The NetworkType of the interface.
+///
+/// The most important options here are Broadcast (which is the default) and PointToPoint.
+/// When PointToPoint is set, then the interface has to have a /32 address and will be treated as
+/// unnumbered.
+///
+/// This roughly serializes to:
+/// ```text
+/// ip ospf network point-to-point
+/// ! or
+/// ip ospf network broadcast
+/// ```
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, PartialOrd, Ord)]
+pub enum NetworkType {
+ Broadcast,
+ NonBroadcast,
+ /// If the interface is unnumbered (i.e. the router-id /32 ip-address is set on the interface).
+ ///
+ /// If OSPF is used in an unnumbered way, you don't need to configure peer-to-peer (e.g. /31)
+ /// addresses at every interface, but you just need to set the router-id at the interface
+ /// (/32). You also need to configure the `ip ospf network point-to-point` FRR option.
+ PointToPoint,
+ PointToMultipoint,
+}
+
+impl Display for NetworkType {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ NetworkType::Broadcast => write!(f, "broadcast"),
+ NetworkType::NonBroadcast => write!(f, "non-broadcast"),
+ NetworkType::PointToPoint => write!(f, "point-to-point"),
+ NetworkType::PointToMultipoint => write!(f, "point-to-multicast"),
+ }
+ }
+}
+
+/// The OSPF interface properties.
+///
+/// The interface gets tied to its fabric by the area property and the FRR `ip ospf area <area>`
+/// command.
+///
+/// This serializes to:
+///
+/// ```text
+/// router ospf
+/// ip ospf area <area>
+/// ip ospf passive <value>
+/// ip ospf network <value>
+/// ```
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, PartialOrd, Ord)]
+pub struct OspfInterface {
+ // Note: an interface can only be a part of a single area(so no vec needed here)
+ pub area: Area,
+ pub passive: Option<bool>,
+ pub network_type: Option<NetworkType>,
+}
+
+impl OspfInterface {
+ pub fn area(&self) -> &Area {
+ &self.area
+ }
+ pub fn passive(&self) -> &Option<bool> {
+ &self.passive
+ }
+ pub fn network_type(&self) -> &Option<NetworkType> {
+ &self.network_type
+ }
+}
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs v3 08/21] frr: add route-map types
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (11 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 07/21] frr: add ospf types Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 09/21] frr: add generic types over openfabric and ospf Stefan Hanreich
` (61 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
Only a very limited subset of the FRR route-maps is implemented here
intially, only what is currently needed for the fabrics feature. Once
standalone route-maps will make it into PVE, we will build on the
structs defined here and add possibly the full featureset. The main
use-case for routemaps in the fabrics is currently to overwrite the
source address in the kernel routing table, so that packets sent via
the fabric contain the router IP rather than the IP of the link (in
the case of point-to-point connections).
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
proxmox-frr/src/lib.rs | 1 +
proxmox-frr/src/route_map.rs | 233 +++++++++++++++++++++++++++++++++++
2 files changed, 234 insertions(+)
create mode 100644 proxmox-frr/src/route_map.rs
diff --git a/proxmox-frr/src/lib.rs b/proxmox-frr/src/lib.rs
index 0d94aef..be9e5c2 100644
--- a/proxmox-frr/src/lib.rs
+++ b/proxmox-frr/src/lib.rs
@@ -1,5 +1,6 @@
pub mod openfabric;
pub mod ospf;
+pub mod route_map;
use std::{fmt::Display, str::FromStr};
use serde::{Deserialize, Serialize};
diff --git a/proxmox-frr/src/route_map.rs b/proxmox-frr/src/route_map.rs
new file mode 100644
index 0000000..4223510
--- /dev/null
+++ b/proxmox-frr/src/route_map.rs
@@ -0,0 +1,233 @@
+use std::{
+ fmt::{self, Display},
+ net::IpAddr,
+};
+
+use proxmox_network_types::ip_address::Cidr;
+
+/// The action for a [`AccessListRule`].
+///
+/// The default is Permit. Deny can be used to create a NOT match (e.g. match all routes that are
+/// NOT in 10.10.10.0/24 using `ip access-list TEST deny 10.10.10.0/24`).
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum AccessAction {
+ Permit,
+ Deny,
+}
+
+impl fmt::Display for AccessAction {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ AccessAction::Permit => write!(f, "permit"),
+ AccessAction::Deny => write!(f, "deny"),
+ }
+ }
+}
+
+/// A single [`AccessList`] rule.
+///
+/// Every rule in a [`AccessList`] is its own command and gets written into a new line (with the
+/// same name). These rules have an action - permit (match) or deny (don't match) - and a network
+/// address (which can be a single address or a range). The seq number is used to differentiate
+/// between access-lists of the same name and rules. Every [`AccessListRule`] has to have a
+/// different seq number.
+/// The `ip` or `ipv6` prefix gets decided based on the Cidr address passed.
+///
+/// This serializes to:
+///
+/// ```text
+/// ip access-list filter permit 10.0.0.0/8
+/// ! or
+/// ipv6 access-list filter permit 2001:db8::/64
+/// ```
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct AccessListRule {
+ pub action: AccessAction,
+ pub network: Cidr,
+ pub seq: Option<u32>,
+}
+
+/// The name of a [`AccessList`].
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub struct AccessListName(String);
+
+impl AccessListName {
+ pub fn new(name: String) -> AccessListName {
+ AccessListName(name)
+ }
+}
+
+impl Display for AccessListName {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+/// A FRR access-list.
+///
+/// Holds a vec of rules. Each rule will get it's own line, FRR will collect all the rules with the
+/// same name and combine them.
+///
+/// This serializes to:
+///
+/// ```text
+/// ip access-list pve_test permit 10.0.0.0/24
+/// ip access-list pve_test permit 12.1.1.0/24
+/// ip access-list pve_test deny 8.8.8.8/32
+/// ```
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct AccessList {
+ pub name: AccessListName,
+ pub rules: Vec<AccessListRule>,
+}
+
+/// A match statement inside a route-map.
+///
+/// A route-map has one or more match statements which decide on which routes the route-map will
+/// execute its actions. If we match on an IP, there are two different syntaxes: `match ip ...` or
+/// `match ipv6 ...`.
+///
+/// Serializes to:
+///
+/// ```text
+/// match ip address <access-list-name>
+/// ! or
+/// match ip next-hop <ip-address>
+/// ! or
+/// match ipv6 address <access-list-name>
+/// ! or
+/// match ipv6 next-hop <ip-address>
+/// ```
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum RouteMapMatch {
+ V4(RouteMapMatchInner),
+ V6(RouteMapMatchInner),
+}
+
+impl Display for RouteMapMatch {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ RouteMapMatch::V4(route_map_match_v4) => match route_map_match_v4 {
+ RouteMapMatchInner::IpAddress(access_list_name) => {
+ write!(f, "match ip address {access_list_name}")
+ }
+ RouteMapMatchInner::IpNextHop(next_hop) => {
+ write!(f, "match ip next-hop {next_hop}")
+ }
+ },
+ RouteMapMatch::V6(route_map_match_v6) => match route_map_match_v6 {
+ RouteMapMatchInner::IpAddress(access_list_name) => {
+ write!(f, "match ipv6 address {access_list_name}")
+ }
+ RouteMapMatchInner::IpNextHop(next_hop) => {
+ write!(f, "match ipv6 next-hop {next_hop}")
+ }
+ },
+ }
+ }
+}
+
+/// A route-map match statement generic on the IP-version.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum RouteMapMatchInner {
+ IpAddress(AccessListName),
+ IpNextHop(String),
+}
+
+/// Defines the Action a route-map takes when it matches on a route.
+///
+/// If the route matches the [`RouteMapMatch`], then a [`RouteMapSet`] action will be executed.
+/// We currently only use the IpSrc command which changes the source address of the route.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum RouteMapSet {
+ LocalPreference(u32),
+ IpSrc(IpAddr),
+ Metric(u32),
+ Community(String),
+}
+
+impl Display for RouteMapSet {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ RouteMapSet::LocalPreference(pref) => write!(f, "set local-preference {}", pref),
+ RouteMapSet::IpSrc(addr) => write!(f, "set src {}", addr),
+ RouteMapSet::Metric(metric) => write!(f, "set metric {}", metric),
+ RouteMapSet::Community(community) => write!(f, "set community {}", community),
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct RouteMapName(String);
+
+impl RouteMapName {
+ pub fn new(name: String) -> RouteMapName {
+ RouteMapName(name)
+ }
+}
+
+impl Display for RouteMapName {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+/// A FRR route-map.
+///
+/// In FRR route-maps are used to manipulate routes learned by protocols. We can match on specific
+/// routes (from specific protocols or subnets) and then change them, by e.g. editing the source
+/// address or adding a metric, bgp community, or local preference.
+///
+/// This serializes to:
+///
+/// ```text
+/// route-map <name> permit 100
+/// match ip address <access-list>
+/// set src <ip-address>
+/// exit
+/// ```
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RouteMap {
+ pub name: RouteMapName,
+ pub seq: u32,
+ pub action: AccessAction,
+ pub matches: Vec<RouteMapMatch>,
+ pub sets: Vec<RouteMapSet>,
+}
+
+/// The ProtocolType used in the [`ProtocolRouteMap`].
+///
+/// Specifies to which protocols we can attach route-maps.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum ProtocolType {
+ Openfabric,
+ Ospf,
+}
+
+impl Display for ProtocolType {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ ProtocolType::Openfabric => write!(f, "openfabric"),
+ ProtocolType::Ospf => write!(f, "ospf"),
+ }
+ }
+}
+
+/// ProtocolRouteMap statement.
+///
+/// This statement attaches the route-map to the protocol, so that all the routes learned through
+/// the specified protocol can be matched on and manipulated with the route-map.
+///
+/// This serializes to:
+///
+/// ```text
+/// ip protocol <protocol> route-map <route-map-name>
+/// ! or
+/// ipv6 protocol <protocol> route-map <route-map-name>
+/// ```
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct ProtocolRouteMap {
+ pub is_ipv6: bool,
+ pub protocol: ProtocolType,
+ pub routemap_name: RouteMapName,
+}
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs v3 09/21] frr: add generic types over openfabric and ospf
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (12 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 08/21] frr: add route-map types Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 10/21] config: sdn: fabrics: add section types Stefan Hanreich
` (60 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
Add generic FRR types that contain openfabric and ospf variants. Also
add the FrrConfig, which holds the whole FRR configuration in a single
struct, which will then be serialized to the FRR configuration file.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
proxmox-frr/src/lib.rs | 106 ++++++++++++++++--
proxmox-frr/src/serializer.rs | 203 ++++++++++++++++++++++++++++++++++
2 files changed, 299 insertions(+), 10 deletions(-)
create mode 100644 proxmox-frr/src/serializer.rs
diff --git a/proxmox-frr/src/lib.rs b/proxmox-frr/src/lib.rs
index be9e5c2..4c093e8 100644
--- a/proxmox-frr/src/lib.rs
+++ b/proxmox-frr/src/lib.rs
@@ -1,23 +1,70 @@
pub mod openfabric;
pub mod ospf;
pub mod route_map;
-use std::{fmt::Display, str::FromStr};
+pub mod serializer;
+use std::{
+ collections::{BTreeMap, BTreeSet},
+ fmt::Display,
+ str::FromStr,
+};
+
+use crate::route_map::{AccessList, ProtocolRouteMap, RouteMap};
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),
+/// Generic FRR router.
+///
+/// This generic FRR router contains all the protocols that we implement.
+/// In FRR this is e.g.:
+/// ```text
+/// router openfabric test
+/// !....
+/// ! or
+/// router ospf
+/// !....
+/// ```
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, PartialOrd, Ord)]
+pub enum Router {
+ Openfabric(openfabric::OpenfabricRouter),
+ Ospf(ospf::OspfRouter),
+}
+
+impl From<openfabric::OpenfabricRouter> for Router {
+ fn from(value: openfabric::OpenfabricRouter) -> Self {
+ Router::Openfabric(value)
+ }
+}
+
+/// Generic FRR routername.
+///
+/// The variants represent different protocols. Some have `router <protocol> <name>`, others have
+/// `router <protocol> <process-id>`, some only have `router <protocol>`.
+#[derive(Clone, Debug, PartialEq, Eq, Hash, SerializeDisplay, PartialOrd, Ord)]
+pub enum RouterName {
+ Openfabric(openfabric::OpenfabricRouterName),
+ Ospf(ospf::OspfRouterName),
+}
+
+impl From<openfabric::OpenfabricRouterName> for RouterName {
+ fn from(value: openfabric::OpenfabricRouterName) -> Self {
+ Self::Openfabric(value)
+ }
}
-/// 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.
+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),
+ }
+ }
+}
+
+/// The interface name is the same on ospf and openfabric, but it is an enum so that we can have
+/// two different entries in the btreemap. This allows us to have an interface in a ospf and
+/// openfabric fabric.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash, PartialOrd, Ord)]
pub enum InterfaceName {
Openfabric(CommonInterfaceName),
@@ -143,3 +190,42 @@ impl Display for CommonInterfaceName {
self.0.fmt(f)
}
}
+
+/// Main FRR config.
+///
+/// Contains the two main frr building blocks: routers and interfaces. It also holds other
+/// top-level FRR options, such as access-lists, router-maps and protocol-routemaps. This struct
+/// gets generated using the `FrrConfigBuilder` in `proxmox-ve-config`.
+#[derive(Clone, Debug, PartialEq, Eq, Default)]
+pub struct FrrConfig {
+ pub router: BTreeMap<RouterName, Router>,
+ pub interfaces: BTreeMap<InterfaceName, Interface>,
+ pub access_lists: Vec<AccessList>,
+ pub routemaps: Vec<RouteMap>,
+ pub protocol_routemaps: BTreeSet<ProtocolRouteMap>,
+}
+
+impl FrrConfig {
+ pub fn new() -> Self {
+ Self::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()
+ }
+
+ pub fn access_lists(&self) -> impl Iterator<Item = &AccessList> + '_ {
+ self.access_lists.iter()
+ }
+ pub fn routemaps(&self) -> impl Iterator<Item = &RouteMap> + '_ {
+ self.routemaps.iter()
+ }
+
+ pub fn protocol_routemaps(&self) -> impl Iterator<Item = &ProtocolRouteMap> + '_ {
+ self.protocol_routemaps.iter()
+ }
+}
diff --git a/proxmox-frr/src/serializer.rs b/proxmox-frr/src/serializer.rs
new file mode 100644
index 0000000..3f8a1fc
--- /dev/null
+++ b/proxmox-frr/src/serializer.rs
@@ -0,0 +1,203 @@
+use std::fmt::{self, Write};
+
+use crate::{
+ openfabric::{OpenfabricInterface, OpenfabricRouter},
+ ospf::{OspfInterface, OspfRouter},
+ route_map::{AccessList, AccessListName, ProtocolRouteMap, RouteMap},
+ FrrConfig, Interface, InterfaceName, Router, RouterName,
+};
+
+pub struct FrrConfigBlob<'a> {
+ buf: &'a mut (dyn Write + 'a),
+}
+
+impl Write for FrrConfigBlob<'_> {
+ fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> {
+ self.buf.write_str(s)
+ }
+}
+
+pub trait FrrSerializer {
+ fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result;
+}
+
+pub fn to_raw_config(frr_config: &FrrConfig) -> Result<Vec<String>, anyhow::Error> {
+ let mut out = String::new();
+ let mut blob = FrrConfigBlob { buf: &mut out };
+ frr_config.serialize(&mut blob)?;
+
+ Ok(out.as_str().lines().map(String::from).collect())
+}
+
+pub fn dump(config: &FrrConfig) -> Result<String, anyhow::Error> {
+ let mut out = String::new();
+ let mut blob = FrrConfigBlob { buf: &mut out };
+ config.serialize(&mut blob)?;
+ Ok(out)
+}
+
+impl FrrSerializer for &FrrConfig {
+ fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
+ self.router().try_for_each(|router| router.serialize(f))?;
+ self.interfaces()
+ .try_for_each(|interface| interface.serialize(f))?;
+ self.access_lists().try_for_each(|list| list.serialize(f))?;
+ self.routemaps().try_for_each(|map| map.serialize(f))?;
+ self.protocol_routemaps()
+ .try_for_each(|pm| pm.serialize(f))?;
+ Ok(())
+ }
+}
+
+impl FrrSerializer for (&RouterName, &Router) {
+ fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
+ let router_name = self.0;
+ let router = self.1;
+ writeln!(f, "router {router_name}")?;
+ router.serialize(f)?;
+ writeln!(f, "exit")?;
+ writeln!(f, "!")?;
+ Ok(())
+ }
+}
+
+impl FrrSerializer for (&InterfaceName, &Interface) {
+ fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
+ let interface_name = self.0;
+ let interface = self.1;
+ writeln!(f, "interface {interface_name}")?;
+ interface.serialize(f)?;
+ writeln!(f, "exit")?;
+ writeln!(f, "!")?;
+ Ok(())
+ }
+}
+
+impl FrrSerializer for (&AccessListName, &AccessList) {
+ fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
+ self.1.serialize(f)?;
+ writeln!(f, "!")
+ }
+}
+
+impl FrrSerializer for Interface {
+ fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
+ match self {
+ Interface::Openfabric(openfabric_interface) => openfabric_interface.serialize(f)?,
+ Interface::Ospf(ospf_interface) => ospf_interface.serialize(f)?,
+ }
+ Ok(())
+ }
+}
+
+impl FrrSerializer for OpenfabricInterface {
+ fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
+ if self.is_ipv6 {
+ writeln!(f, " ipv6 router {}", self.fabric_id())?;
+ }
+ if self.is_ipv4 {
+ writeln!(f, " ip router {}", self.fabric_id())?;
+ }
+ if self.passive() == Some(true) {
+ writeln!(f, " openfabric passive")?;
+ }
+ if let Some(interval) = self.hello_interval() {
+ writeln!(f, " openfabric hello-interval {interval}",)?;
+ }
+ if let Some(multiplier) = self.hello_multiplier() {
+ writeln!(f, " openfabric hello-multiplier {multiplier}",)?;
+ }
+ if let Some(interval) = self.csnp_interval() {
+ writeln!(f, " openfabric csnp-interval {interval}",)?;
+ }
+ Ok(())
+ }
+}
+
+impl FrrSerializer for OspfInterface {
+ fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
+ writeln!(f, " ip ospf {}", self.area())?;
+ if *self.passive() == Some(true) {
+ writeln!(f, " ip ospf passive")?;
+ }
+ if let Some(network_type) = self.network_type() {
+ writeln!(f, " ip ospf network {network_type}")?;
+ }
+ Ok(())
+ }
+}
+
+impl FrrSerializer for &Router {
+ fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
+ match self {
+ Router::Openfabric(open_fabric_router) => open_fabric_router.serialize(f),
+ Router::Ospf(ospf_router) => ospf_router.serialize(f),
+ }
+ }
+}
+
+impl FrrSerializer for &OpenfabricRouter {
+ fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
+ writeln!(f, " net {}", self.net())?;
+ Ok(())
+ }
+}
+
+impl FrrSerializer for &OspfRouter {
+ fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
+ writeln!(f, " ospf router-id {}", self.router_id())?;
+ Ok(())
+ }
+}
+
+impl FrrSerializer for &AccessList {
+ fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
+ for i in &self.rules {
+ if i.network.is_ipv6() {
+ write!(f, "ipv6 ")?;
+ }
+ write!(f, "access-list {} ", self.name)?;
+ if let Some(seq) = i.seq {
+ write!(f, "seq {seq} ")?;
+ }
+ write!(f, "{} ", i.action)?;
+ writeln!(f, "{}", i.network)?;
+ }
+ writeln!(f, "!")?;
+ Ok(())
+ }
+}
+
+impl FrrSerializer for &RouteMap {
+ fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
+ writeln!(f, "route-map {} {} {}", self.name, self.action, self.seq)?;
+ for i in &self.matches {
+ writeln!(f, " {}", i)?;
+ }
+ for i in &self.sets {
+ writeln!(f, " {}", i)?;
+ }
+ writeln!(f, "exit")?;
+ writeln!(f, "!")
+ }
+}
+
+impl FrrSerializer for &ProtocolRouteMap {
+ fn serialize(&self, f: &mut FrrConfigBlob<'_>) -> fmt::Result {
+ if self.is_ipv6 {
+ writeln!(
+ f,
+ "ipv6 protocol {} route-map {}",
+ self.protocol, self.routemap_name
+ )?;
+ } else {
+ writeln!(
+ f,
+ "ip protocol {} route-map {}",
+ self.protocol, self.routemap_name
+ )?;
+ }
+ writeln!(f, "!")?;
+ Ok(())
+ }
+}
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs v3 10/21] config: sdn: fabrics: add section types
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (13 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 09/21] frr: add generic types over openfabric and ospf Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 11/21] config: sdn: fabrics: add node " Stefan Hanreich
` (59 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
The fabrics configuration consists of two different entity types:
fabrics and nodes. This commit adds support for the fabrics inside
this section configuration.
There are properties required for all fabrics, regardless of type,
which are the ID of the fabric as well as the IP prefix of the fabric.
The FabricSection contains those shared properties, in order to avoid
having to duplicate them and any methods requiring them for every
protocol. This means that every concrete fabric section type is just
an instance of the generic type FabricSection<T>.
The type parameter is used to define properties, that are specific to
a protocol (e.g. area in OSPF). We also provide a generic
implementation for e.g. ApiType without having to avoid having to
repeat the API definitions and Updater types for the common properties
multiple times.
We also create the necessary types for updating a FabricSection<T> via
FabricSectionUpdater and FabricDeletableProperties, and provide
generic implementations for ApiType / Updater on FabricSection /
FabricSectionUpdater where possible.
This design allows to add new protocols simply by defining their
protocol-specific properties in a struct and then using it with the
generic FabricSection type.
The ID of a fabric is at most 8 alphanumeric characters long and can
additionally include hyphens. It cannot start or end with a hyphen.
This is because the name of the fabric, will be used as name for
network interfaces, which can be at most 15 characters long on Linux.
This leaves us enough space for generating interfaces with
pre/suffixes. This is analogous to the IDs of many other SDN entities,
e.g. VNet where the same restrictions apply.
The structs here will later be used in parsing the whole configuration
file, located in '/etc/pve/sdn/fabrics.cfg'.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/Cargo.toml | 6 +-
proxmox-ve-config/debian/control | 8 ++
proxmox-ve-config/src/sdn/fabric/mod.rs | 1 +
.../src/sdn/fabric/section_config/fabric.rs | 133 ++++++++++++++++++
.../src/sdn/fabric/section_config/mod.rs | 1 +
proxmox-ve-config/src/sdn/mod.rs | 1 +
6 files changed, 148 insertions(+), 2 deletions(-)
create mode 100644 proxmox-ve-config/src/sdn/fabric/mod.rs
create mode 100644 proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
create mode 100644 proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml
index c240a87..7674dc7 100644
--- a/proxmox-ve-config/Cargo.toml
+++ b/proxmox-ve-config/Cargo.toml
@@ -10,6 +10,8 @@ exclude.workspace = true
log = "0.4"
anyhow = { workspace = true }
nix = "0.26"
+regex = { workspace = true }
+const_format = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true, features = [ "derive" ] }
@@ -18,7 +20,7 @@ serde_plain = "1"
serde_with = { workspace = true }
proxmox-serde = { version = "0.1.2", features = [ "perl" ]}
-proxmox-network-types = { workspace = true }
-proxmox-schema = "4"
+proxmox-network-types = { workspace = true, features = [ "api-types" ] }
+proxmox-schema = { workspace = true, features = [ "api-types" ] }
proxmox-sys = "0.6.4"
proxmox-sortable-macro = "0.1.3"
diff --git a/proxmox-ve-config/debian/control b/proxmox-ve-config/debian/control
index b76904a..f416316 100644
--- a/proxmox-ve-config/debian/control
+++ b/proxmox-ve-config/debian/control
@@ -7,14 +7,18 @@ Build-Depends-Arch: cargo:native <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
librust-anyhow-1+default-dev <!nocheck>,
+ librust-const-format-0.2+default-dev <!nocheck>,
librust-log-0.4+default-dev <!nocheck>,
librust-nix-0.26+default-dev <!nocheck>,
+ librust-proxmox-network-types-0.1+api-types-dev <!nocheck>,
librust-proxmox-network-types-0.1+default-dev <!nocheck>,
+ librust-proxmox-schema-4+api-types-dev <!nocheck>,
librust-proxmox-schema-4+default-dev <!nocheck>,
librust-proxmox-serde-0.1+default-dev (>= 0.1.2-~~) <!nocheck>,
librust-proxmox-serde-0.1+perl-dev (>= 0.1.2-~~) <!nocheck>,
librust-proxmox-sortable-macro-0.1+default-dev (>= 0.1.3-~~) <!nocheck>,
librust-proxmox-sys-0.6+default-dev (>= 0.6.4-~~) <!nocheck>,
+ librust-regex-1+default-dev (>= 1.7-~~) <!nocheck>,
librust-serde-1+default-dev <!nocheck>,
librust-serde-1+derive-dev <!nocheck>,
librust-serde-json-1+default-dev <!nocheck>,
@@ -34,14 +38,18 @@ Multi-Arch: same
Depends:
${misc:Depends},
librust-anyhow-1+default-dev,
+ librust-const-format-0.2+default-dev,
librust-log-0.4+default-dev,
librust-nix-0.26+default-dev,
+ librust-proxmox-network-types-0.1+api-types-dev,
librust-proxmox-network-types-0.1+default-dev,
+ librust-proxmox-schema-4+api-types-dev,
librust-proxmox-schema-4+default-dev,
librust-proxmox-serde-0.1+default-dev (>= 0.1.2-~~),
librust-proxmox-serde-0.1+perl-dev (>= 0.1.2-~~),
librust-proxmox-sortable-macro-0.1+default-dev (>= 0.1.3-~~),
librust-proxmox-sys-0.6+default-dev (>= 0.6.4-~~),
+ librust-regex-1+default-dev (>= 1.7-~~),
librust-serde-1+default-dev,
librust-serde-1+derive-dev,
librust-serde-json-1+default-dev,
diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs
new file mode 100644
index 0000000..007be6a
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/mod.rs
@@ -0,0 +1 @@
+pub mod section_config;
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
new file mode 100644
index 0000000..9787d5d
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
@@ -0,0 +1,133 @@
+use const_format::concatcp;
+use serde::{Deserialize, Serialize};
+
+use proxmox_network_types::ip_address::{Ipv4Cidr, Ipv6Cidr};
+use proxmox_schema::{
+ api, api_string_type, const_regex, AllOfSchema, ApiStringFormat, ApiType, ObjectSchema, Schema,
+ Updater,
+};
+
+pub const FABRIC_ID_REGEX_STR: &str = r"(?:[a-zA-Z0-9])(?:[a-zA-Z0-9\-]){0,6}(?:[a-zA-Z0-9])?";
+
+const_regex! {
+ pub FABRIC_ID_REGEX = concatcp!(r"^", FABRIC_ID_REGEX_STR, r"$");
+}
+
+pub const FABRIC_ID_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&FABRIC_ID_REGEX);
+
+api_string_type! {
+ /// ID of an SDN fabric.
+ #[api(format: &FABRIC_ID_FORMAT)]
+ #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
+ pub struct FabricId(String);
+}
+
+/// A fabric section in an SDN fabric config.
+///
+/// This struct contains all the properties that are required for any fabric, regardless of
+/// protocol. Properties that are specific to a protocol can be passed via the type parameter.
+///
+/// This is mainly used by the [`Fabric`] and [`super::Section`] enums to specify which types of fabrics can exist,
+/// without having to re-define common properties for every fabric. It also simplifies accessing
+/// common properties by encapsulating the specific properties to [`FabricSection<T>::properties`].
+#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
+pub struct FabricSection<T> {
+ pub(crate) id: FabricId,
+
+ /// IPv4 Prefix that contains the Node IPs.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) ip_prefix: Option<Ipv4Cidr>,
+
+ /// IPv6 Prefix that contains the Node IPs.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) ip6_prefix: Option<Ipv6Cidr>,
+
+ #[serde(flatten)]
+ pub(crate) properties: T,
+}
+
+impl<T> FabricSection<T> {
+ /// Get the protocol-specific properties of [FabricSection].
+ pub fn properties(&self) -> &T {
+ &self.properties
+ }
+
+ /// Get a mutable reference to the protocol-specific properties of [FabricSection].
+ pub fn properties_mut(&mut self) -> &mut T {
+ &mut self.properties
+ }
+
+ /// Get the id of [FabricSection].
+ pub fn id(&self) -> &FabricId {
+ &self.id
+ }
+
+ /// Get the ip-prefix (IPv4 CIDR) of [FabricSection].
+ pub fn ip_prefix(&self) -> Option<Ipv4Cidr> {
+ self.ip_prefix
+ }
+
+ /// Get the ip6-prefix (IPv6 CIDR) of [FabricSection].
+ pub fn ip6_prefix(&self) -> Option<Ipv6Cidr> {
+ self.ip6_prefix
+ }
+}
+
+const FABRIC_SECTION_SCHEMA: Schema = ObjectSchema::new(
+ "Common properties for fabrics in an SDN fabric.",
+ &[
+ ("id", false, &FabricId::API_SCHEMA),
+ ("ip6_prefix", true, &Ipv6Cidr::API_SCHEMA),
+ ("ip_prefix", true, &Ipv4Cidr::API_SCHEMA),
+ ],
+)
+.schema();
+
+impl<T: ApiType> ApiType for FabricSection<T> {
+ const API_SCHEMA: Schema = AllOfSchema::new(
+ "Fabric in an SDN fabric.",
+ &[&FABRIC_SECTION_SCHEMA, &T::API_SCHEMA],
+ )
+ .schema();
+}
+
+/// Updater for a [`FabricSection<T>`]
+///
+/// This specifies the updater type for the common properties in [`FabricSection<T>`], as well as
+/// provides the delete property for deleting properties on updates.
+///
+/// It also provides a blanket implementation of [`Updater`] for any type parameter that implements
+/// Updater as well.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct FabricSectionUpdater<T, D> {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) ip_prefix: Option<Ipv4Cidr>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) ip6_prefix: Option<Ipv6Cidr>,
+
+ #[serde(flatten)]
+ pub(crate) properties: T,
+
+ #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
+ pub(crate) delete: Vec<FabricDeletableProperties<D>>,
+}
+
+impl<T: Updater, D> Updater for FabricSectionUpdater<T, D> {
+ fn is_empty(&self) -> bool {
+ T::is_empty(&self.properties)
+ && self.ip_prefix.is_none()
+ && self.ip6_prefix.is_none()
+ && self.delete.is_empty()
+ }
+}
+
+/// Deletable properties for a [`FabricSection<T>`]
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case", untagged)]
+pub enum FabricDeletableProperties<T> {
+ IpPrefix,
+ Ip6Prefix,
+ #[serde(untagged)]
+ Protocol(T),
+}
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
new file mode 100644
index 0000000..8106b6c
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
@@ -0,0 +1 @@
+pub mod fabric;
diff --git a/proxmox-ve-config/src/sdn/mod.rs b/proxmox-ve-config/src/sdn/mod.rs
index cde6fed..7a46db3 100644
--- a/proxmox-ve-config/src/sdn/mod.rs
+++ b/proxmox-ve-config/src/sdn/mod.rs
@@ -1,4 +1,5 @@
pub mod config;
+pub mod fabric;
pub mod ipam;
use std::{error::Error, fmt::Display, str::FromStr};
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs v3 11/21] config: sdn: fabrics: add node section types
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (14 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 10/21] config: sdn: fabrics: add section types Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 12/21] config: sdn: fabrics: add interface name struct Stefan Hanreich
` (58 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
NodeSection functions identically to the FabricSection type. It
contains all the common properties that nodes from all protocols have.
Protocol-specific properties can be defined via the type parameter of
NodeSection. It also provides generic implementations for ApiType, so
if the type parameter implements ApiType, then NodeSection<T> also
implements ApiType.
Together, FabricSection and NodeSection represent the two different
types of entities in the fabric section configuration, fabrics and
nodes.
IP addresses are optional because this enables nodes to be part of a
fabric without advertising an IP themselves. This enables nodes to
import routes from the fabric without announcing a route to
themselves. Also, since there can be either IPv4 or IPv6 (or both)
set, they have to be optional anyway.
The ID of a node is defined as the hostname of a node in the fabric,
but since nodes can be part of multiple fabrics their section config
entry can only be uniquely identified by a combination of the ID of
the fabric they belong to and the ID of the node. For this reason, the
ID of a node in the section config consists of the ID of the fabric as
well as the ID of the node, separated by an underscore. We provide a
helper struct for parsing the section ID into its two separate
components, so we can easily parse the ID on deserializing the section
config and easily serialize it back into its composite form when
serializing.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
.../src/sdn/fabric/section_config/mod.rs | 1 +
.../src/sdn/fabric/section_config/node.rs | 169 ++++++++++++++++++
2 files changed, 170 insertions(+)
create mode 100644 proxmox-ve-config/src/sdn/fabric/section_config/node.rs
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
index 8106b6c..0ca5695 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
@@ -1 +1,2 @@
pub mod fabric;
+pub mod node;
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
new file mode 100644
index 0000000..b1202a2
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
@@ -0,0 +1,169 @@
+use const_format::concatcp;
+use proxmox_schema::api_types::{IP_V4_SCHEMA, IP_V6_SCHEMA};
+use serde::{Deserialize, Serialize};
+use serde_with::{DeserializeFromStr, SerializeDisplay};
+
+use proxmox_network_types::ip_address::api_types::{Ipv4Addr, Ipv6Addr};
+
+use proxmox_schema::{
+ api, api_string_type, const_regex, AllOfSchema, ApiStringFormat, ApiType, ObjectSchema, Schema,
+ StringSchema, UpdaterType,
+};
+
+use crate::sdn::fabric::section_config::{
+ fabric::{FabricId, FABRIC_ID_REGEX_STR},
+};
+
+pub const NODE_ID_REGEX_STR: &str = r"(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]){0,61}(?:[a-zA-Z0-9]){0,1})";
+
+const_regex! {
+ pub NODE_ID_REGEX = concatcp!(r"^", NODE_ID_REGEX_STR, r"$");
+ pub NODE_SECTION_ID_REGEX = concatcp!(r"^", FABRIC_ID_REGEX_STR, r"_", NODE_ID_REGEX_STR, r"$");
+}
+
+pub const NODE_ID_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&NODE_ID_REGEX);
+pub const NODE_SECTION_ID_FORMAT: ApiStringFormat =
+ ApiStringFormat::Pattern(&NODE_SECTION_ID_REGEX);
+
+api_string_type! {
+ /// ID of a node in an SDN fabric.
+ ///
+ /// This corresponds to the hostname of the node.
+ #[api(format: &NODE_ID_FORMAT)]
+ #[derive(Debug, Deserialize, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, UpdaterType)]
+ pub struct NodeId(String);
+}
+
+/// ID of a node in the section config.
+///
+/// This corresponds to the ID of the fabric, that contains this node, as well as the hostname of
+/// the node. They are joined by an underscore.
+///
+/// This struct is a helper for parsing the string into the two separate parts. It (de-)serializes
+/// from and into a String.
+#[derive(
+ Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, SerializeDisplay, DeserializeFromStr,
+)]
+pub struct NodeSectionId {
+ pub(crate) fabric_id: FabricId,
+ pub(crate) node_id: NodeId,
+}
+
+impl ApiType for NodeSectionId {
+ const API_SCHEMA: Schema = StringSchema::new("ID of a SDN node in the section config")
+ .format(&NODE_SECTION_ID_FORMAT)
+ .schema();
+}
+
+impl NodeSectionId {
+ /// Build a new [NodeSectionId] from the passed [FabricId] and [NodeId].
+ pub fn new(fabric_id: FabricId, node_id: NodeId) -> Self {
+ Self { fabric_id, node_id }
+ }
+
+ /// Get the fabric part of the [NodeSectionId].
+ pub fn fabric_id(&self) -> &FabricId {
+ &self.fabric_id
+ }
+
+ /// Get the node part of the [NodeSectionId].
+ pub fn node_id(&self) -> &NodeId {
+ &self.node_id
+ }
+}
+
+impl std::str::FromStr for NodeSectionId {
+ type Err = anyhow::Error;
+
+ fn from_str(value: &str) -> Result<Self, Self::Err> {
+ let (fabric_id, node_id) = value.split_once("_").unwrap();
+
+ Ok(Self {
+ fabric_id: FabricId::from_string(fabric_id.to_string())?,
+ node_id: NodeId::from_string(node_id.to_string())?,
+ })
+ }
+}
+
+impl std::fmt::Display for NodeSectionId {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "{}_{}", self.fabric_id.as_ref(), self.node_id)
+ }
+}
+
+const NODE_SECTION_SCHEMA: Schema = ObjectSchema::new(
+ "Common properties for a node in an SDN fabric.",
+ &[
+ ("id", false, &NodeSectionId::API_SCHEMA),
+ ("ip", true, &IP_V4_SCHEMA),
+ ("ip6", true, &IP_V6_SCHEMA),
+ ],
+)
+.schema();
+
+/// A node section in an SDN fabric config.
+///
+/// This struct contains all the properties that are required for any node, regardless of
+/// protocol. Properties that are specific to a protocol can be passed via the type parameter.
+///
+/// This is mainly used by the [Node] and [super::Section] enums to specify which types of nodes can exist,
+/// without having to re-define common properties for every node. It also simplifies accessing
+/// common properties by encapsulating the specific properties to [NodeSection<T>::properties].
+#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
+pub struct NodeSection<T> {
+ pub(crate) id: NodeSectionId,
+
+ /// IPv4 for this node in the fabric
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) ip: Option<Ipv4Addr>,
+
+ /// IPv6 for this node in the fabric
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) ip6: Option<Ipv6Addr>,
+
+ #[serde(flatten)]
+ pub(crate) properties: T,
+}
+
+impl<T> NodeSection<T> {
+ /// Get the protocol-specific properties of the [NodeSection].
+ pub fn properties(&self) -> &T {
+ &self.properties
+ }
+
+ /// Get a mutable reference to the protocol-specific properties of the [NodeSection].
+ pub fn properties_mut(&mut self) -> &mut T {
+ &mut self.properties
+ }
+
+ /// Get the id of the [NodeSection].
+ pub fn id(&self) -> &NodeSectionId {
+ &self.id
+ }
+
+ /// Get the IPv4 address (Router-ID) of the [NodeSection].
+ ///
+ /// Either the [NodeSection::ip] (IPv4) address or the [NodeSection::ip6] (IPv6) address *must*
+ /// be set. This is checked during the validation, so it's guaranteed. OpenFabric can also be
+ /// used dual-stack, so both IPv4 and IPv6 addresses can be set.
+ pub fn ip(&self) -> Option<std::net::Ipv4Addr> {
+ self.ip.as_deref().copied()
+ }
+
+ /// Get the IPv6 address (Router-ID) of the [NodeSection].
+ ///
+ /// Either the [NodeSection::ip] (IPv4) address or the [NodeSection::ip6] (IPv6) address *must*
+ /// be set. This is checked during the validation, so it's guaranteed. OpenFabric can also be
+ /// used dual-stack, so both IPv4 and IPv6 addresses can be set.
+ pub fn ip6(&self) -> Option<std::net::Ipv6Addr> {
+ self.ip6.as_deref().copied()
+ }
+}
+
+impl<T: ApiType> ApiType for NodeSection<T> {
+ const API_SCHEMA: Schema = AllOfSchema::new(
+ "Node in an SDN fabric.",
+ &[&NODE_SECTION_SCHEMA, &T::API_SCHEMA],
+ )
+ .schema();
+}
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs v3 12/21] config: sdn: fabrics: add interface name struct
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (15 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 11/21] config: sdn: fabrics: add node " Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 13/21] config: sdn: fabrics: add openfabric properties Stefan Hanreich
` (57 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
A simple String wrapper that represents the name of a network
interface. We are restricting the interface name to ASCII-only
characters via the regex, since otherwise the length check would fail,
due to String being Unicode. While network interface names can be
arbitrary bytes and don't correspond to a specific encoding,
restricting it to ASCII seemed sensible here. We can always lift the
restriction later if this is required.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
.../sdn/fabric/section_config/interface.rs | 22 +++++++++++++++++++
.../src/sdn/fabric/section_config/mod.rs | 1 +
2 files changed, 23 insertions(+)
create mode 100644 proxmox-ve-config/src/sdn/fabric/section_config/interface.rs
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/interface.rs b/proxmox-ve-config/src/sdn/fabric/section_config/interface.rs
new file mode 100644
index 0000000..4374f38
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/interface.rs
@@ -0,0 +1,22 @@
+use serde::{Deserialize, Serialize};
+
+use proxmox_schema::{api, api_string_type, const_regex, ApiStringFormat, UpdaterType};
+
+const_regex! {
+ pub INTERFACE_NAME_REGEX = r"^[[:ascii:]]+$";
+}
+
+pub const INTERFACE_NAME_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&INTERFACE_NAME_REGEX);
+
+api_string_type! {
+ /// Name of a network interface.
+ ///
+ /// The interface name can have a maximum of 15 characters. This is a kernel limit.
+ #[api(
+ min_length: 1,
+ max_length: 15,
+ format: &INTERFACE_NAME_FORMAT,
+ )]
+ #[derive(Debug, Deserialize, Serialize, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, UpdaterType)]
+ pub struct InterfaceName(String);
+}
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
index 0ca5695..b61bc43 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
@@ -1,2 +1,3 @@
pub mod fabric;
+pub mod interface;
pub mod node;
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs v3 13/21] config: sdn: fabrics: add openfabric properties
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (16 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 12/21] config: sdn: fabrics: add interface name struct Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 14/21] config: sdn: fabrics: add ospf properties Stefan Hanreich
` (56 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
This commit adds the protocol-specific properties, that are required
for an Openfabric fabric. They correspond to the respective properties
in the FRR fabricd configuration. For more information, see the FRR
documentation [1].
While the hello interval and csnp interval could be set on a
per-interface basis, it is recommended to keep them the same across
the whole fabric. This is why they can only be configured globally and
the value will be applied to all interfaces that are part of the
fabric. We expose the hello multiplier in the interface properties, so
users can define a longer hello interval on a per-interface basis by
supplying the hello multiplier parameter. The upside of this is, that
everything scales from a single value and users can just edit the
hello interval in the fabric and everything else will adjust based on
that setting.
We also introduce two new general enums: Fabric and Node. They contain
the concrete FabricSection and NodeSection types that are used for
each protocol and add the Openfabric sections to it. We provide
dedicated updater structs for the enum as well, that fall back to the
concrete updater structs of the respective variant.
New protocols can simply be added by adding another variant
to the Fabric and Node enums.
[1] https://docs.frrouting.org/en/latest/fabricd.html
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/Cargo.toml | 2 +
proxmox-ve-config/debian/control | 4 +
.../src/sdn/fabric/section_config/fabric.rs | 85 +++++++++++++-
.../src/sdn/fabric/section_config/mod.rs | 1 +
.../src/sdn/fabric/section_config/node.rs | 46 ++++++++
.../sdn/fabric/section_config/protocol/mod.rs | 1 +
.../section_config/protocol/openfabric.rs | 105 ++++++++++++++++++
7 files changed, 243 insertions(+), 1 deletion(-)
create mode 100644 proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs
create mode 100644 proxmox-ve-config/src/sdn/fabric/section_config/protocol/openfabric.rs
diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml
index 7674dc7..c5aeba9 100644
--- a/proxmox-ve-config/Cargo.toml
+++ b/proxmox-ve-config/Cargo.toml
@@ -22,5 +22,7 @@ proxmox-serde = { version = "0.1.2", features = [ "perl" ]}
proxmox-network-types = { workspace = true, features = [ "api-types" ] }
proxmox-schema = { workspace = true, features = [ "api-types" ] }
+proxmox-sdn-types = { workspace = true }
+proxmox-section-config = { version = "3" }
proxmox-sys = "0.6.4"
proxmox-sortable-macro = "0.1.3"
diff --git a/proxmox-ve-config/debian/control b/proxmox-ve-config/debian/control
index f416316..57d7987 100644
--- a/proxmox-ve-config/debian/control
+++ b/proxmox-ve-config/debian/control
@@ -14,6 +14,8 @@ Build-Depends-Arch: cargo:native <!nocheck>,
librust-proxmox-network-types-0.1+default-dev <!nocheck>,
librust-proxmox-schema-4+api-types-dev <!nocheck>,
librust-proxmox-schema-4+default-dev <!nocheck>,
+ librust-proxmox-sdn-types-0.1+default-dev <!nocheck>,
+ librust-proxmox-section-config-3+default-dev <!nocheck>,
librust-proxmox-serde-0.1+default-dev (>= 0.1.2-~~) <!nocheck>,
librust-proxmox-serde-0.1+perl-dev (>= 0.1.2-~~) <!nocheck>,
librust-proxmox-sortable-macro-0.1+default-dev (>= 0.1.3-~~) <!nocheck>,
@@ -45,6 +47,8 @@ Depends:
librust-proxmox-network-types-0.1+default-dev,
librust-proxmox-schema-4+api-types-dev,
librust-proxmox-schema-4+default-dev,
+ librust-proxmox-sdn-types-0.1+default-dev,
+ librust-proxmox-section-config-3+default-dev,
librust-proxmox-serde-0.1+default-dev (>= 0.1.2-~~),
librust-proxmox-serde-0.1+perl-dev (>= 0.1.2-~~),
librust-proxmox-sortable-macro-0.1+default-dev (>= 0.1.3-~~),
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
index 9787d5d..3e56e2b 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
@@ -4,7 +4,11 @@ use serde::{Deserialize, Serialize};
use proxmox_network_types::ip_address::{Ipv4Cidr, Ipv6Cidr};
use proxmox_schema::{
api, api_string_type, const_regex, AllOfSchema, ApiStringFormat, ApiType, ObjectSchema, Schema,
- Updater,
+ Updater, UpdaterType,
+};
+
+use crate::sdn::fabric::section_config::protocol::openfabric::{
+ OpenfabricDeletableProperties, OpenfabricProperties, OpenfabricPropertiesUpdater,
};
pub const FABRIC_ID_REGEX_STR: &str = r"(?:[a-zA-Z0-9])(?:[a-zA-Z0-9\-]){0,6}(?:[a-zA-Z0-9])?";
@@ -122,6 +126,85 @@ impl<T: Updater, D> Updater for FabricSectionUpdater<T, D> {
}
}
+impl UpdaterType for FabricSection<OpenfabricProperties> {
+ type Updater = FabricSectionUpdater<OpenfabricPropertiesUpdater, OpenfabricDeletableProperties>;
+}
+
+/// Enum containing all types of fabrics.
+///
+/// It utilizes [`FabricSection<T>`] to define all possible types of fabrics. For parsing the
+/// configuration, please use the [`Section`] enum, which contains the Node sections as well. This
+/// struct is used for sorting the sections into their sub-types after parsing the configuration
+/// via [`Section`].
+#[api(
+ "id-property": "id",
+ "id-schema": {
+ type: String,
+ description: "Fabric ID",
+ format: &FABRIC_ID_FORMAT,
+ },
+ "type-key": "protocol",
+)]
+#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
+#[serde(rename_all = "snake_case", tag = "protocol")]
+pub enum Fabric {
+ Openfabric(FabricSection<OpenfabricProperties>),
+}
+
+impl UpdaterType for Fabric {
+ type Updater = FabricUpdater;
+}
+
+impl Fabric {
+ /// Get the id of the [Fabric].
+ ///
+ /// This is a common property for all protocols.
+ pub fn id(&self) -> &FabricId {
+ match self {
+ Self::Openfabric(fabric_section) => fabric_section.id(),
+ }
+ }
+
+ /// Get the ip-prefix (IPv4 CIDR) of the [Fabric].
+ ///
+ /// This is a common property for all protocols.
+ pub fn ip_prefix(&self) -> Option<Ipv4Cidr> {
+ match self {
+ Fabric::Openfabric(fabric_section) => fabric_section.ip_prefix(),
+ }
+ }
+
+ /// Get the ip6-prefix (IPv6 CIDR) of the [Fabric].
+ ///
+ /// This is a common property for all protocols.
+ pub fn ip6_prefix(&self) -> Option<Ipv6Cidr> {
+ match self {
+ Fabric::Openfabric(fabric_section) => fabric_section.ip6_prefix(),
+ }
+ }
+}
+
+impl From<FabricSection<OpenfabricProperties>> for Fabric {
+ fn from(section: FabricSection<OpenfabricProperties>) -> Self {
+ Fabric::Openfabric(section)
+ }
+}
+
+/// Enum containing all updater types for fabrics
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case", tag = "protocol")]
+pub enum FabricUpdater {
+ Openfabric(<FabricSection<OpenfabricProperties> as UpdaterType>::Updater),
+}
+
+impl Updater for FabricUpdater {
+ fn is_empty(&self) -> bool {
+ match self {
+ FabricUpdater::Openfabric(updater) => updater.is_empty(),
+ }
+ }
+}
+
/// Deletable properties for a [`FabricSection<T>`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", untagged)]
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
index b61bc43..7db3788 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
@@ -1,3 +1,4 @@
pub mod fabric;
pub mod interface;
pub mod node;
+pub mod protocol;
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
index b1202a2..510bfde 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
@@ -12,6 +12,7 @@ use proxmox_schema::{
use crate::sdn::fabric::section_config::{
fabric::{FabricId, FABRIC_ID_REGEX_STR},
+ protocol::openfabric::OpenfabricNodeProperties,
};
pub const NODE_ID_REGEX_STR: &str = r"(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]){0,61}(?:[a-zA-Z0-9]){0,1})";
@@ -167,3 +168,48 @@ impl<T: ApiType> ApiType for NodeSection<T> {
)
.schema();
}
+
+/// Enum containing all types of nodes.
+#[api(
+ "id-property": "id",
+ "id-schema": {
+ type: String,
+ description: "Node ID",
+ format: &NODE_ID_FORMAT,
+ },
+ "type-key": "protocol",
+)]
+#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
+#[serde(rename_all = "snake_case", tag = "protocol")]
+pub enum Node {
+ Openfabric(NodeSection<OpenfabricNodeProperties>),
+}
+
+impl Node {
+ /// Get the id of the [Node].
+ pub fn id(&self) -> &NodeSectionId {
+ match self {
+ Node::Openfabric(node_section) => node_section.id(),
+ }
+ }
+
+ /// Get the ip (IPv4) of the [Node].
+ pub fn ip(&self) -> Option<std::net::Ipv4Addr> {
+ match self {
+ Node::Openfabric(node_section) => node_section.ip(),
+ }
+ }
+
+ /// Get the ip (IPv6) of the [Node].
+ pub fn ip6(&self) -> Option<std::net::Ipv6Addr> {
+ match self {
+ Node::Openfabric(node_section) => node_section.ip6(),
+ }
+ }
+}
+
+impl From<NodeSection<OpenfabricNodeProperties>> for Node {
+ fn from(value: NodeSection<OpenfabricNodeProperties>) -> Self {
+ Self::Openfabric(value)
+ }
+}
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs
new file mode 100644
index 0000000..e5b800b
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs
@@ -0,0 +1 @@
+pub mod openfabric;
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/openfabric.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/openfabric.rs
new file mode 100644
index 0000000..156ff2b
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/openfabric.rs
@@ -0,0 +1,105 @@
+use std::ops::Deref;
+
+use proxmox_network_types::ip_address::{Ipv4Cidr, Ipv6Cidr};
+use serde::{Deserialize, Serialize};
+
+use proxmox_schema::{api, property_string::PropertyString, ApiStringFormat, Updater};
+use proxmox_sdn_types::openfabric::{CsnpInterval, HelloInterval, HelloMultiplier};
+
+use crate::sdn::fabric::section_config::interface::InterfaceName;
+
+/// Protocol-specific options for an OpenFabric Fabric.
+#[api]
+#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)]
+pub struct OpenfabricProperties {
+ /// This will be distributed to all interfaces on every node. 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.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) hello_interval: Option<HelloInterval>,
+
+ /// This will be distributed to all interfaces on every node.The Complete Sequence Number
+ /// Packets (CSNP) interval in seconds. The interval range is 1 to 600.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) csnp_interval: Option<CsnpInterval>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
+#[serde(rename_all = "snake_case")]
+pub enum OpenfabricDeletableProperties {
+ HelloInterval,
+ CsnpInterval,
+}
+
+/// Properties for an OpenFabric node
+#[api(
+ properties: {
+ interfaces: {
+ type: Array,
+ optional: true,
+ items: {
+ type: String,
+ description: "OpenFabric interface",
+ format: &ApiStringFormat::PropertyString(&OpenfabricInterfaceProperties::API_SCHEMA),
+ }
+ },
+ }
+)]
+#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)]
+pub struct OpenfabricNodeProperties {
+ /// Interfaces for this node
+ #[serde(default)]
+ pub(crate) interfaces: Vec<PropertyString<OpenfabricInterfaceProperties>>,
+}
+
+impl OpenfabricNodeProperties {
+ /// Returns an interator over all the interfaces.
+ pub fn interfaces(&self) -> impl Iterator<Item = &OpenfabricInterfaceProperties> {
+ self.interfaces
+ .iter()
+ .map(|property_string| property_string.deref())
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum OpenfabricNodeDeletableProperties {
+ Interfaces,
+}
+
+/// Properties for an OpenFabric interface
+#[api]
+#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)]
+pub struct OpenfabricInterfaceProperties {
+ pub(crate) name: InterfaceName,
+
+ /// The multiplier for the hello holding time on a given interface. The range is 2 to
+ /// 100.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) hello_multiplier: Option<HelloMultiplier>,
+
+ /// If ip and ip6 are unset, then this is an point-to-point interface
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) ip: Option<Ipv4Cidr>,
+
+ /// If ip6 and ip are unset, then this is an point-to-point interface
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) ip6: Option<Ipv6Cidr>,
+}
+
+impl OpenfabricInterfaceProperties {
+ /// Get the name of the interface.
+ pub fn name(&self) -> &InterfaceName {
+ &self.name
+ }
+
+ /// Get the ip (IPv4) of the interface.
+ pub fn ip(&self) -> Option<Ipv4Cidr> {
+ self.ip
+ }
+
+ /// Get the ip6 (IPv6) of the interface.
+ pub fn ip6(&self) -> Option<Ipv6Cidr> {
+ self.ip6
+ }
+}
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs v3 14/21] config: sdn: fabrics: add ospf properties
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (17 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 13/21] config: sdn: fabrics: add openfabric properties Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 15/21] config: sdn: fabrics: add api types Stefan Hanreich
` (55 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
Define the protocol-specific properties for OSPF and add the concrete
section types to the Fabric and Node enum. Currently only area is
included, which is also the only property that is required by FRR. We
wanted to start with a minimal set of properties and add any options
later on, depending on feedback from users.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
.../src/sdn/fabric/section_config/fabric.rs | 23 ++++-
.../src/sdn/fabric/section_config/node.rs | 12 ++-
.../sdn/fabric/section_config/protocol/mod.rs | 1 +
.../fabric/section_config/protocol/ospf.rs | 85 +++++++++++++++++++
4 files changed, 118 insertions(+), 3 deletions(-)
create mode 100644 proxmox-ve-config/src/sdn/fabric/section_config/protocol/ospf.rs
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
index 3e56e2b..8ecf725 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
@@ -7,8 +7,11 @@ use proxmox_schema::{
Updater, UpdaterType,
};
-use crate::sdn::fabric::section_config::protocol::openfabric::{
- OpenfabricDeletableProperties, OpenfabricProperties, OpenfabricPropertiesUpdater,
+use crate::sdn::fabric::section_config::protocol::{
+ openfabric::{
+ OpenfabricDeletableProperties, OpenfabricProperties, OpenfabricPropertiesUpdater,
+ },
+ ospf::{OspfDeletableProperties, OspfProperties, OspfPropertiesUpdater},
};
pub const FABRIC_ID_REGEX_STR: &str = r"(?:[a-zA-Z0-9])(?:[a-zA-Z0-9\-]){0,6}(?:[a-zA-Z0-9])?";
@@ -130,6 +133,10 @@ impl UpdaterType for FabricSection<OpenfabricProperties> {
type Updater = FabricSectionUpdater<OpenfabricPropertiesUpdater, OpenfabricDeletableProperties>;
}
+impl UpdaterType for FabricSection<OspfProperties> {
+ type Updater = FabricSectionUpdater<OspfPropertiesUpdater, OspfDeletableProperties>;
+}
+
/// Enum containing all types of fabrics.
///
/// It utilizes [`FabricSection<T>`] to define all possible types of fabrics. For parsing the
@@ -149,6 +156,7 @@ impl UpdaterType for FabricSection<OpenfabricProperties> {
#[serde(rename_all = "snake_case", tag = "protocol")]
pub enum Fabric {
Openfabric(FabricSection<OpenfabricProperties>),
+ Ospf(FabricSection<OspfProperties>),
}
impl UpdaterType for Fabric {
@@ -162,6 +170,7 @@ impl Fabric {
pub fn id(&self) -> &FabricId {
match self {
Self::Openfabric(fabric_section) => fabric_section.id(),
+ Self::Ospf(fabric_section) => fabric_section.id(),
}
}
@@ -171,6 +180,7 @@ impl Fabric {
pub fn ip_prefix(&self) -> Option<Ipv4Cidr> {
match self {
Fabric::Openfabric(fabric_section) => fabric_section.ip_prefix(),
+ Fabric::Ospf(fabric_section) => fabric_section.ip_prefix(),
}
}
@@ -180,6 +190,7 @@ impl Fabric {
pub fn ip6_prefix(&self) -> Option<Ipv6Cidr> {
match self {
Fabric::Openfabric(fabric_section) => fabric_section.ip6_prefix(),
+ Fabric::Ospf(fabric_section) => fabric_section.ip6_prefix(),
}
}
}
@@ -190,17 +201,25 @@ impl From<FabricSection<OpenfabricProperties>> for Fabric {
}
}
+impl From<FabricSection<OspfProperties>> for Fabric {
+ fn from(section: FabricSection<OspfProperties>) -> Self {
+ Fabric::Ospf(section)
+ }
+}
+
/// Enum containing all updater types for fabrics
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "protocol")]
pub enum FabricUpdater {
Openfabric(<FabricSection<OpenfabricProperties> as UpdaterType>::Updater),
+ Ospf(<FabricSection<OspfProperties> as UpdaterType>::Updater),
}
impl Updater for FabricUpdater {
fn is_empty(&self) -> bool {
match self {
FabricUpdater::Openfabric(updater) => updater.is_empty(),
+ FabricUpdater::Ospf(updater) => updater.is_empty(),
}
}
}
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
index 510bfde..bd5ffea 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
@@ -12,7 +12,7 @@ use proxmox_schema::{
use crate::sdn::fabric::section_config::{
fabric::{FabricId, FABRIC_ID_REGEX_STR},
- protocol::openfabric::OpenfabricNodeProperties,
+ protocol::{openfabric::OpenfabricNodeProperties, ospf::OspfNodeProperties},
};
pub const NODE_ID_REGEX_STR: &str = r"(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]){0,61}(?:[a-zA-Z0-9]){0,1})";
@@ -183,6 +183,7 @@ impl<T: ApiType> ApiType for NodeSection<T> {
#[serde(rename_all = "snake_case", tag = "protocol")]
pub enum Node {
Openfabric(NodeSection<OpenfabricNodeProperties>),
+ Ospf(NodeSection<OspfNodeProperties>),
}
impl Node {
@@ -190,6 +191,7 @@ impl Node {
pub fn id(&self) -> &NodeSectionId {
match self {
Node::Openfabric(node_section) => node_section.id(),
+ Node::Ospf(node_section) => node_section.id(),
}
}
@@ -197,6 +199,7 @@ impl Node {
pub fn ip(&self) -> Option<std::net::Ipv4Addr> {
match self {
Node::Openfabric(node_section) => node_section.ip(),
+ Node::Ospf(node_section) => node_section.ip(),
}
}
@@ -204,6 +207,7 @@ impl Node {
pub fn ip6(&self) -> Option<std::net::Ipv6Addr> {
match self {
Node::Openfabric(node_section) => node_section.ip6(),
+ Node::Ospf(node_section) => node_section.ip6(),
}
}
}
@@ -213,3 +217,9 @@ impl From<NodeSection<OpenfabricNodeProperties>> for Node {
Self::Openfabric(value)
}
}
+
+impl From<NodeSection<OspfNodeProperties>> for Node {
+ fn from(value: NodeSection<OspfNodeProperties>) -> Self {
+ Self::Ospf(value)
+ }
+}
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs
index e5b800b..c1ec847 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs
@@ -1 +1,2 @@
pub mod openfabric;
+pub mod ospf;
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/ospf.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/ospf.rs
new file mode 100644
index 0000000..4c368fa
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/ospf.rs
@@ -0,0 +1,85 @@
+use std::ops::Deref;
+
+use proxmox_network_types::ip_address::Ipv4Cidr;
+use proxmox_sdn_types::area::Area;
+use serde::{Deserialize, Serialize};
+
+use proxmox_schema::{api, property_string::PropertyString, ApiStringFormat, Updater};
+
+use crate::sdn::fabric::section_config::interface::InterfaceName;
+
+#[api]
+#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)]
+/// Properties for an Ospf fabric.
+pub struct OspfProperties {
+ /// OSPF area
+ pub(crate) area: Area,
+}
+
+impl OspfProperties {
+ pub fn set_area(&mut self, value: Area) {
+ self.area = value;
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case", untagged)]
+pub enum OspfDeletableProperties {}
+
+#[api(
+ properties: {
+ interfaces: {
+ type: Array,
+ optional: true,
+ items: {
+ type: String,
+ description: "Properties for an Ospf interface.",
+ format: &ApiStringFormat::PropertyString(&OspfInterfaceProperties::API_SCHEMA),
+ }
+ },
+ }
+)]
+#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)]
+/// Properties for an Ospf node.
+pub struct OspfNodeProperties {
+ /// Interfaces for this Node.
+ #[serde(default)]
+ pub(crate) interfaces: Vec<PropertyString<OspfInterfaceProperties>>,
+}
+
+impl OspfNodeProperties {
+ pub fn interfaces(&self) -> impl Iterator<Item = &OspfInterfaceProperties> {
+ self.interfaces
+ .iter()
+ .map(|property_string| property_string.deref())
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case", untagged)]
+pub enum OspfNodeDeletableProperties {
+ Interfaces,
+}
+
+#[api]
+#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)]
+/// Properties for an OSPF interface.
+pub struct OspfInterfaceProperties {
+ pub(crate) name: InterfaceName,
+
+ /// If IP is unset, then this is an unnumbered interface
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) ip: Option<Ipv4Cidr>,
+}
+
+impl OspfInterfaceProperties {
+ /// Get the name of the OSPF interface.
+ pub fn name(&self) -> &InterfaceName {
+ &self.name
+ }
+
+ /// Get the ip (IPv4) of the OSPF interface.
+ pub fn ip(&self) -> Option<Ipv4Cidr> {
+ self.ip
+ }
+}
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs v3 15/21] config: sdn: fabrics: add api types
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (18 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 14/21] config: sdn: fabrics: add ospf properties Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 16/21] config: sdn: fabrics: add section config Stefan Hanreich
` (54 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
Add an api submodule to the section config module, that provides the
types that are intended to be returned and accepted by the Perl API in
Proxmox VE. This allows us to decouple the format returned in the API
from the configuration format.
This is particularly relevant in the case of the NodeSection type.
While the section config stores the composite ID of the node as the ID
of the section in the section config (and therefore as a single string
/ property), we want to be able to return them as independent fields
from the API, to avoid having to parse the ID everywhere else we want
to use it. Thanks to the generic NodeSection type we only have to
define the conversion from / to the API type once, while the
protocol-specific types can stay the same.
For the fabrics, we simply re-use the section_config types for now,
but by re-exporting them as type alias we are more flexible in
possibly changing the API types or the underlying section config types
later on.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
.../src/sdn/fabric/section_config/fabric.rs | 5 +
.../src/sdn/fabric/section_config/node.rs | 159 ++++++++++++++++++
2 files changed, 164 insertions(+)
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
index 8ecf725..75a3093 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
@@ -233,3 +233,8 @@ pub enum FabricDeletableProperties<T> {
#[serde(untagged)]
Protocol(T),
}
+
+pub mod api {
+ pub type Fabric = super::Fabric;
+ pub type FabricUpdater = super::FabricUpdater;
+}
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
index bd5ffea..b1b2034 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
@@ -223,3 +223,162 @@ impl From<NodeSection<OspfNodeProperties>> for Node {
Self::Ospf(value)
}
}
+
+/// API types for SDN fabric node configurations.
+///
+/// This module provides specialized types that are used for API interactions when retrieving,
+/// creating, or updating fabric/node configurations. These types serialize differently than their
+/// section-config configuration counterparts to be nicer client-side.
+///
+/// The module includes:
+/// - [NodeData<T>]: API-friendly version of [NodeSection<T>] that flattens the node identifier
+/// into separate `fabric_id` and `node_id` fields
+/// - [Node]: API-version of [super::Node]
+/// - [NodeDataUpdater]
+/// - [NodeDeletableProperties]
+///
+/// These types include conversion methods to transform between API representations and internal
+/// configuration objects.
+pub mod api {
+ use serde::{Deserialize, Serialize};
+
+ use proxmox_schema::{Updater, UpdaterType};
+
+ use crate::sdn::fabric::section_config::protocol::{
+ openfabric::{
+ OpenfabricNodeDeletableProperties, OpenfabricNodeProperties,
+ OpenfabricNodePropertiesUpdater,
+ },
+ ospf::{OspfNodeDeletableProperties, OspfNodeProperties, OspfNodePropertiesUpdater},
+ };
+
+ use super::*;
+
+ /// API-equivalent to [NodeSection<T>].
+ ///
+ /// The difference is that instead of serializing fabric_id and node_id into a single string
+ /// (`{fabric_id}_{node_id}`), are serialized normally as two distinct properties. This
+ /// prevents us from needing to parse the node_id in the frontend using `split("_")`.
+ #[derive(Debug, Clone, Serialize, Deserialize)]
+ pub struct NodeData<T> {
+ fabric_id: FabricId,
+ node_id: NodeId,
+
+ /// IPv4 for this node in the Ospf fabric
+ #[serde(skip_serializing_if = "Option::is_none")]
+ ip: Option<Ipv4Addr>,
+
+ /// IPv6 for this node in the Ospf fabric
+ #[serde(skip_serializing_if = "Option::is_none")]
+ ip6: Option<Ipv6Addr>,
+
+ #[serde(flatten)]
+ properties: T,
+ }
+
+ impl<T> From<NodeSection<T>> for NodeData<T> {
+ fn from(value: NodeSection<T>) -> Self {
+ Self {
+ fabric_id: value.id.fabric_id,
+ node_id: value.id.node_id,
+ ip: value.ip,
+ ip6: value.ip6,
+ properties: value.properties,
+ }
+ }
+ }
+
+ impl<T> From<NodeData<T>> for NodeSection<T> {
+ fn from(value: NodeData<T>) -> Self {
+ let id = NodeSectionId::new(value.fabric_id, value.node_id);
+
+ Self {
+ id,
+ ip: value.ip,
+ ip6: value.ip6,
+ properties: value.properties,
+ }
+ }
+ }
+
+ /// API-equivalent to [super::Node].
+ #[derive(Debug, Clone, Serialize, Deserialize)]
+ #[serde(rename_all = "snake_case", tag = "protocol")]
+ pub enum Node {
+ Openfabric(NodeData<OpenfabricNodeProperties>),
+ Ospf(NodeData<OspfNodeProperties>),
+ }
+
+ impl From<super::Node> for Node {
+ fn from(value: super::Node) -> Self {
+ match value {
+ super::Node::Openfabric(node_section) => Self::Openfabric(node_section.into()),
+ super::Node::Ospf(node_section) => Self::Ospf(node_section.into()),
+ }
+ }
+ }
+
+ impl From<Node> for super::Node {
+ fn from(value: Node) -> Self {
+ match value {
+ Node::Openfabric(node_section) => Self::Openfabric(node_section.into()),
+ Node::Ospf(node_section) => Self::Ospf(node_section.into()),
+ }
+ }
+ }
+
+ impl UpdaterType for NodeData<OpenfabricNodeProperties> {
+ type Updater =
+ NodeDataUpdater<OpenfabricNodePropertiesUpdater, OpenfabricNodeDeletableProperties>;
+ }
+
+ impl UpdaterType for NodeData<OspfNodeProperties> {
+ type Updater = NodeDataUpdater<OspfNodePropertiesUpdater, OspfNodeDeletableProperties>;
+ }
+
+ #[derive(Debug, Clone, Serialize, Deserialize)]
+ pub struct NodeDataUpdater<T, D> {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) ip: Option<Ipv4Addr>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) ip6: Option<Ipv6Addr>,
+
+ #[serde(flatten)]
+ pub(crate) properties: T,
+
+ #[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
+ pub(crate) delete: Vec<NodeDeletableProperties<D>>,
+ }
+
+ impl<T: UpdaterType + Updater, D> UpdaterType for NodeDataUpdater<T, D> {
+ type Updater = NodeDataUpdater<T::Updater, D>;
+ }
+
+ impl<T: Updater, D> Updater for NodeDataUpdater<T, D> {
+ fn is_empty(&self) -> bool {
+ T::is_empty(&self.properties)
+ && self.ip.is_none()
+ && self.ip6.is_none()
+ && self.delete.is_empty()
+ }
+ }
+
+ #[derive(Debug, Clone, Serialize, Deserialize)]
+ #[serde(rename_all = "snake_case", tag = "protocol")]
+ pub enum NodeUpdater {
+ Openfabric(
+ NodeDataUpdater<OpenfabricNodePropertiesUpdater, OpenfabricNodeDeletableProperties>,
+ ),
+ Ospf(NodeDataUpdater<OspfNodePropertiesUpdater, OspfNodeDeletableProperties>),
+ }
+
+ #[derive(Debug, Clone, Serialize, Deserialize)]
+ #[serde(rename_all = "snake_case")]
+ pub enum NodeDeletableProperties<T> {
+ Ip,
+ Ip6,
+ #[serde(untagged)]
+ Protocol(T),
+ }
+}
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs v3 16/21] config: sdn: fabrics: add section config
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (19 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 15/21] config: sdn: fabrics: add api types Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 17/21] config: sdn: fabrics: add fabric config Stefan Hanreich
` (53 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
The Section type represents the configuration as stored in
'/etc/pve/sdn/fabrics.cfg'. It can contain a mix of fabric and node
sections, which are in turn unique to each protocol. We utilize the
generic FabricSection and NodeSection types to define every possible
section type in the section config. We also provide a helper for
sorting the sections in the configuration file into Fabrics and Nodes,
as well as conversion from / to the Fabric and Node types.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
.../src/sdn/fabric/section_config/mod.rs | 105 ++++++++++++++++++
1 file changed, 105 insertions(+)
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
index 7db3788..174ea4d 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
@@ -2,3 +2,108 @@ pub mod fabric;
pub mod interface;
pub mod node;
pub mod protocol;
+
+use const_format::concatcp;
+use serde::{Deserialize, Serialize};
+
+use crate::sdn::fabric::section_config::{
+ fabric::{Fabric, FabricSection, FABRIC_ID_REGEX_STR},
+ node::{Node, NodeSection, NODE_ID_REGEX_STR},
+ protocol::{
+ openfabric::{OpenfabricNodeProperties, OpenfabricProperties},
+ ospf::{OspfNodeProperties, OspfProperties},
+ },
+};
+
+use proxmox_schema::{api, const_regex, ApiStringFormat};
+
+/// Represents a value that can be one of two given types.
+///
+/// This is used for the fabrics section config, where values could either be Fabrics or Nodes. It
+/// can be used to split the sections contained in the config into their concrete types safely.
+pub enum Either<L, R> {
+ Left(L),
+ Right(R),
+}
+
+impl From<Section> for Either<Fabric, Node> {
+ fn from(section: Section) -> Self {
+ match section {
+ Section::OpenfabricFabric(fabric_section) => Self::Left(fabric_section.into()),
+ Section::OspfFabric(fabric_section) => Self::Left(fabric_section.into()),
+ Section::OpenfabricNode(node_section) => Self::Right(node_section.into()),
+ Section::OspfNode(node_section) => Self::Right(node_section.into()),
+ }
+ }
+}
+
+const_regex! {
+ pub SECTION_ID_REGEX = concatcp!(r"^", FABRIC_ID_REGEX_STR, r"(?:_", NODE_ID_REGEX_STR, r")?$");
+}
+
+pub const SECTION_ID_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&SECTION_ID_REGEX);
+
+/// A section in the SDN fabrics config.
+///
+/// It contains two variants for every protocol: The fabric and the node. They are represented
+/// respectively by [`FabricSection`] and [`NodeSection`] which encapsulate the common properties
+/// of fabrics and nodes and take the specific properties for the protocol as a type parameter.
+#[api(
+ "id-property": "id",
+ "id-schema": {
+ type: String,
+ description: "fabric/node id",
+ format: &SECTION_ID_FORMAT,
+ },
+ "type-key": "type",
+)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case", tag = "type")]
+pub enum Section {
+ OpenfabricFabric(FabricSection<OpenfabricProperties>),
+ OspfFabric(FabricSection<OspfProperties>),
+ OpenfabricNode(NodeSection<OpenfabricNodeProperties>),
+ OspfNode(NodeSection<OspfNodeProperties>),
+}
+
+impl From<FabricSection<OpenfabricProperties>> for Section {
+ fn from(section: FabricSection<OpenfabricProperties>) -> Self {
+ Self::OpenfabricFabric(section)
+ }
+}
+
+impl From<FabricSection<OspfProperties>> for Section {
+ fn from(section: FabricSection<OspfProperties>) -> Self {
+ Self::OspfFabric(section)
+ }
+}
+
+impl From<NodeSection<OpenfabricNodeProperties>> for Section {
+ fn from(section: NodeSection<OpenfabricNodeProperties>) -> Self {
+ Self::OpenfabricNode(section)
+ }
+}
+
+impl From<NodeSection<OspfNodeProperties>> for Section {
+ fn from(section: NodeSection<OspfNodeProperties>) -> Self {
+ Self::OspfNode(section)
+ }
+}
+
+impl From<Fabric> for Section {
+ fn from(fabric: Fabric) -> Self {
+ match fabric {
+ Fabric::Openfabric(fabric_section) => fabric_section.into(),
+ Fabric::Ospf(fabric_section) => fabric_section.into(),
+ }
+ }
+}
+
+impl From<Node> for Section {
+ fn from(node: Node) -> Self {
+ match node {
+ Node::Openfabric(node_section) => node_section.into(),
+ Node::Ospf(node_section) => node_section.into(),
+ }
+ }
+}
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs v3 17/21] config: sdn: fabrics: add fabric config
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (20 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 16/21] config: sdn: fabrics: add section config Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 18/21] common: sdn: fabrics: implement validation Stefan Hanreich
` (52 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
The FabricConfig transforms the flat section configuration into its
hierarchical representation, which makes querying and validating the
fabrics configuration more ergonomic.
It provides the CRUD methods for safely manipulating the fabric
configuration, while checking for possible errors. It is intended to
be the interface for external users to use the fabric configuration.
By encapsulating the configuration into this struct, we can always
assure that invariants are upheld and many of them can actually be
checked at compile time (e.g. proper combination of FabricSection and
NodeSection in the hierarchy via the Entry struct).
It uses the Fabric and Node enums foremost in its public API, so
adding new protocols does not change the public API. This enables us
to write generic API methods that do not need to be updated when
adding new protocols.
If so desired, users can still access the protocol-specific properties
by matching on the FabricEntry enum.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/src/sdn/fabric/mod.rs | 516 ++++++++++++++++++++++++
1 file changed, 516 insertions(+)
diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs
index 007be6a..3342a70 100644
--- a/proxmox-ve-config/src/sdn/fabric/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/mod.rs
@@ -1 +1,517 @@
pub mod section_config;
+
+use std::collections::BTreeMap;
+use std::marker::PhantomData;
+use std::ops::Deref;
+
+use serde::{Deserialize, Serialize};
+
+use crate::sdn::fabric::section_config::{
+ fabric::{
+ Fabric, FabricDeletableProperties, FabricId, FabricSection, FabricSectionUpdater,
+ FabricUpdater,
+ },
+ node::{
+ api::{NodeDataUpdater, NodeDeletableProperties, NodeUpdater},
+ Node, NodeId, NodeSection,
+ },
+ protocol::{
+ openfabric::{
+ OpenfabricDeletableProperties, OpenfabricNodeDeletableProperties,
+ OpenfabricNodeProperties, OpenfabricNodePropertiesUpdater, OpenfabricProperties,
+ OpenfabricPropertiesUpdater,
+ },
+ ospf::{
+ OspfDeletableProperties, OspfNodeDeletableProperties, OspfNodeProperties,
+ OspfNodePropertiesUpdater, OspfProperties, OspfPropertiesUpdater,
+ },
+ },
+};
+
+#[derive(thiserror::Error, Debug)]
+pub enum FabricConfigError {
+ #[error("fabric '{0}' does not exist in configuration")]
+ FabricDoesNotExist(String),
+ #[error("node '{0}' does not exist in fabric '{1}'")]
+ NodeDoesNotExist(String, String),
+ #[error("node has a different protocol than the referenced fabric")]
+ ProtocolMismatch,
+ #[error("fabric '{0}' already exists in config")]
+ DuplicateFabric(String),
+ #[error("node '{0}' already exists in config for fabric {1}")]
+ DuplicateNode(String, String),
+ // should usually not occur, but we still check for it nonetheless
+ #[error("mismatched fabric_id")]
+ FabricIdMismatch,
+}
+
+/// An entry in a [`FabricConfig`].
+///
+/// It enforces compatible types for its containing [`FabricSection`] and [`NodeSection`] via the
+/// generic parameters, so only Nodes and Fabrics with compatible types can be inserted into an
+/// entry.
+#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
+pub struct Entry<F, N> {
+ // we want to store the enum structs Fabric & Node here, in order to have access to the
+ // properties and methods defined on the enum itself.
+ // In order to still be able to type-check that an Entry contains the right combination of
+ // NodeSection and FabricSection, we type hint the actual types wrapped into Fabric & Node here
+ // via PhantomData and only allow insertion of the proper types via the provided methods.
+ #[serde(skip)]
+ _phantom_fabric: PhantomData<FabricSection<F>>,
+ #[serde(skip)]
+ _phantom_node: PhantomData<NodeSection<N>>,
+
+ fabric: Fabric,
+ nodes: BTreeMap<NodeId, Node>,
+}
+
+impl<F, N> Entry<F, N>
+where
+ Fabric: From<FabricSection<F>>,
+ Node: From<NodeSection<N>>,
+{
+ /// Create a new [`Entry`] from the passed [`FabricSection<F>`] with no nodes.
+ fn new(fabric: FabricSection<F>) -> Self {
+ Self {
+ fabric: fabric.into(),
+ nodes: Default::default(),
+ _phantom_fabric: Default::default(),
+ _phantom_node: Default::default(),
+ }
+ }
+
+ /// Adds a node to this entry
+ ///
+ /// # Errors
+ /// Returns an error if the node's fabric_id doesn't match this entry's fabric_id
+ /// or if a node with the same ID already exists in this entry.
+ fn add_node(&mut self, node: NodeSection<N>) -> Result<(), FabricConfigError> {
+ if self.nodes.contains_key(node.id().node_id()) {
+ return Err(FabricConfigError::DuplicateNode(
+ node.id().node_id().to_string(),
+ self.fabric.id().to_string(),
+ ));
+ }
+
+ if node.id().fabric_id() != self.fabric.id() {
+ return Err(FabricConfigError::FabricIdMismatch);
+ }
+
+ self.nodes.insert(node.id().node_id().clone(), node.into());
+
+ Ok(())
+ }
+
+ /// Get a reference to the node with the passed node_id. Return an error if the node doesn't exist.
+ fn get_node(&self, id: &NodeId) -> Result<&Node, FabricConfigError> {
+ self.nodes.get(id).ok_or_else(|| {
+ FabricConfigError::NodeDoesNotExist(id.to_string(), self.fabric.id().to_string())
+ })
+ }
+
+ /// Get a mutable reference to the Node with the passed node_id.
+ fn get_node_mut(&mut self, id: &NodeId) -> Result<&mut Node, FabricConfigError> {
+ self.nodes.get_mut(id).ok_or_else(|| {
+ FabricConfigError::NodeDoesNotExist(id.to_string(), self.fabric.id().to_string())
+ })
+ }
+
+ /// Removes and returns a node with the specified node_id from this entry.
+ ///
+ /// # Errors
+ /// Returns `FabricConfigError::NodeDoesNotExist` if no node with the given node_id exists.
+ fn delete_node(&mut self, id: &NodeId) -> Result<Node, FabricConfigError> {
+ self.nodes.remove(id).ok_or_else(|| {
+ FabricConfigError::NodeDoesNotExist(id.to_string(), self.fabric.id().to_string())
+ })
+ }
+
+ /// Get entry as a (Fabric, Vec<Node>) pair. This consumes the Entry.
+ fn into_pair(self) -> (Fabric, Vec<Node>) {
+ (self.fabric, self.nodes.into_values().collect())
+ }
+}
+
+impl Entry<OpenfabricProperties, OpenfabricNodeProperties> {
+ /// Get the OpenFabric fabric config.
+ ///
+ /// This method is implemented for [Entry<OpenfabricProperties, OpenfabricNodeProperties>],
+ /// so it is guaranteed that a [FabricSection<OpenfabricProperties>] is returned.
+ pub fn fabric_section(&self) -> &FabricSection<OpenfabricProperties> {
+ if let Fabric::Openfabric(section) = &self.fabric {
+ return section;
+ }
+
+ unreachable!();
+ }
+
+ /// Get the OpenFabric node config for the given node_id.
+ ///
+ /// This method is implemented for [Entry<OpenfabricProperties, OpenfabricNodeProperties>],
+ /// so it is guaranteed that a [NodeSection<OpenfabricNodeProperties>] is returned.
+ /// An error is returned if the node is not found.
+ pub fn node_section(
+ &self,
+ id: &NodeId,
+ ) -> Result<&NodeSection<OpenfabricNodeProperties>, FabricConfigError> {
+ if let Node::Openfabric(section) = self.get_node(id)? {
+ return Ok(section);
+ }
+
+ unreachable!();
+ }
+}
+
+impl Entry<OspfProperties, OspfNodeProperties> {
+ /// Get the OSPF fabric config.
+ ///
+ /// This method is implemented for [Entry<OspfProperties, OspfNodeProperties>],
+ /// so it is guaranteed that a [FabricSection<OspfProperties>] is returned.
+ pub fn fabric_section(&self) -> &FabricSection<OspfProperties> {
+ if let Fabric::Ospf(section) = &self.fabric {
+ return section;
+ }
+
+ unreachable!();
+ }
+
+ /// Get the OSPF node config for the given node_id.
+ ///
+ /// This method is implemented for [Entry<OspfProperties, OspfNodeProperties>],
+ /// so it is guaranteed that a [NodeSection<OspfNodeProperties>] is returned.
+ /// An error is returned if the node is not found.
+ pub fn node_section(
+ &self,
+ id: &NodeId,
+ ) -> Result<&NodeSection<OspfNodeProperties>, FabricConfigError> {
+ if let Node::Ospf(section) = self.get_node(id)? {
+ return Ok(section);
+ }
+
+ unreachable!();
+ }
+}
+
+/// All possible entries in a [`FabricConfig`].
+///
+/// It utilizes the [`Entry`] struct to validate proper combinations of [`FabricSection`] and
+/// [`NodeSection`].
+#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
+pub enum FabricEntry {
+ Openfabric(Entry<OpenfabricProperties, OpenfabricNodeProperties>),
+ Ospf(Entry<OspfProperties, OspfNodeProperties>),
+}
+
+impl FabricEntry {
+ /// Adds a node to the fabric entry.
+ /// The node must match the protocol type of the fabric entry.
+ pub fn add_node(&mut self, node: Node) -> Result<(), FabricConfigError> {
+ match (self, node) {
+ (FabricEntry::Openfabric(entry), Node::Openfabric(node_section)) => {
+ entry.add_node(node_section)
+ }
+ (FabricEntry::Ospf(entry), Node::Ospf(node_section)) => entry.add_node(node_section),
+ _ => Err(FabricConfigError::ProtocolMismatch),
+ }
+ }
+
+ /// Get a reference to a Node specified by the node_id. Returns an error if the node is not
+ /// found.
+ pub fn get_node(&self, id: &NodeId) -> Result<&Node, FabricConfigError> {
+ match self {
+ FabricEntry::Openfabric(entry) => entry.get_node(id),
+ FabricEntry::Ospf(entry) => entry.get_node(id),
+ }
+ }
+
+ /// Get a mutable reference to a Node specified by the node_id. Returns an error if the node is not
+ /// found.
+ pub fn get_node_mut(&mut self, id: &NodeId) -> Result<&mut Node, FabricConfigError> {
+ match self {
+ FabricEntry::Openfabric(entry) => entry.get_node_mut(id),
+ FabricEntry::Ospf(entry) => entry.get_node_mut(id),
+ }
+ }
+
+ /// Update the Node with the specified node_id using the passed [NodeUpdater].
+ pub fn update_node(
+ &mut self,
+ id: &NodeId,
+ updater: NodeUpdater,
+ ) -> Result<(), FabricConfigError> {
+ let node = self.get_node_mut(id)?;
+
+ match (node, updater) {
+ (Node::Openfabric(node_section), NodeUpdater::Openfabric(updater)) => {
+ let NodeDataUpdater::<
+ OpenfabricNodePropertiesUpdater,
+ OpenfabricNodeDeletableProperties,
+ > {
+ ip,
+ ip6,
+ properties: OpenfabricNodePropertiesUpdater { interfaces },
+ delete,
+ } = updater;
+
+ if let Some(ip) = ip {
+ node_section.ip = Some(ip);
+ }
+
+ if let Some(ip) = ip6 {
+ node_section.ip6 = Some(ip);
+ }
+
+ if let Some(interfaces) = interfaces {
+ node_section.properties.interfaces = interfaces;
+ }
+
+ for property in delete {
+ match property {
+ NodeDeletableProperties::Ip => node_section.ip = None,
+ NodeDeletableProperties::Ip6 => node_section.ip6 = None,
+ NodeDeletableProperties::Protocol(
+ OpenfabricNodeDeletableProperties::Interfaces,
+ ) => node_section.properties.interfaces = Vec::new(),
+ }
+ }
+
+ Ok(())
+ }
+ (Node::Ospf(node_section), NodeUpdater::Ospf(updater)) => {
+ let NodeDataUpdater::<OspfNodePropertiesUpdater, OspfNodeDeletableProperties> {
+ ip,
+ ip6,
+ properties: OspfNodePropertiesUpdater { interfaces },
+ delete,
+ } = updater;
+
+ if let Some(ip) = ip {
+ node_section.ip = Some(ip);
+ }
+
+ if let Some(ip) = ip6 {
+ node_section.ip6 = Some(ip);
+ }
+
+ if let Some(interfaces) = interfaces {
+ node_section.properties.interfaces = interfaces;
+ }
+
+ for property in delete {
+ match property {
+ NodeDeletableProperties::Ip => node_section.ip = None,
+ NodeDeletableProperties::Ip6 => node_section.ip6 = None,
+ NodeDeletableProperties::Protocol(
+ OspfNodeDeletableProperties::Interfaces,
+ ) => node_section.properties.interfaces = Vec::new(),
+ }
+ }
+
+ Ok(())
+ }
+ _ => Err(FabricConfigError::ProtocolMismatch),
+ }
+ }
+
+ /// Get an iterator over all the nodes in this fabric.
+ pub fn nodes(&self) -> impl Iterator<Item = (&NodeId, &Node)> + '_ {
+ match self {
+ FabricEntry::Openfabric(entry) => entry.nodes.iter(),
+ FabricEntry::Ospf(entry) => entry.nodes.iter(),
+ }
+ }
+
+ /// Delete the node specified with the node_id. Returns an error if it doesn't exist.
+ pub fn delete_node(&mut self, id: &NodeId) -> Result<Node, FabricConfigError> {
+ match self {
+ FabricEntry::Openfabric(entry) => entry.delete_node(id),
+ FabricEntry::Ospf(entry) => entry.delete_node(id),
+ }
+ }
+
+ /// Consume this entry and return a (Fabric, Vec<Node>) pair. This is used to write to the
+ /// section-config file.
+ pub fn into_section_config(self) -> (Fabric, Vec<Node>) {
+ match self {
+ FabricEntry::Openfabric(entry) => entry.into_pair(),
+ FabricEntry::Ospf(entry) => entry.into_pair(),
+ }
+ }
+
+ /// Get a reference to the Fabric.
+ pub fn fabric(&self) -> &Fabric {
+ match self {
+ FabricEntry::Openfabric(entry) => &entry.fabric,
+ FabricEntry::Ospf(entry) => &entry.fabric,
+ }
+ }
+
+ /// Get a mutable reference to the Fabric.
+ pub fn fabric_mut(&mut self) -> &mut Fabric {
+ match self {
+ FabricEntry::Openfabric(entry) => &mut entry.fabric,
+ FabricEntry::Ospf(entry) => &mut entry.fabric,
+ }
+ }
+}
+
+impl From<Fabric> for FabricEntry {
+ fn from(fabric: Fabric) -> Self {
+ match fabric {
+ Fabric::Openfabric(fabric_section) => {
+ FabricEntry::Openfabric(Entry::new(fabric_section))
+ }
+ Fabric::Ospf(fabric_section) => FabricEntry::Ospf(Entry::new(fabric_section)),
+ }
+ }
+}
+
+/// A complete SDN fabric configuration.
+///
+/// This struct contains the whole fabric configuration in a tree-like structure (fabrics -> nodes
+/// -> interfaces).
+#[derive(Default, Debug, Serialize, Deserialize, Clone, Hash)]
+pub struct FabricConfig {
+ fabrics: BTreeMap<FabricId, FabricEntry>,
+}
+
+impl Deref for FabricConfig {
+ type Target = BTreeMap<FabricId, FabricEntry>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.fabrics
+ }
+}
+
+impl FabricConfig {
+ /// Add a fabric to the [FabricConfig].
+ ///
+ /// Returns an error if a fabric with the same name exists.
+ pub fn add_fabric(&mut self, fabric: Fabric) -> Result<(), FabricConfigError> {
+ if self.fabrics.contains_key(fabric.id()) {
+ return Err(FabricConfigError::DuplicateFabric(fabric.id().to_string()));
+ }
+
+ self.fabrics.insert(fabric.id().clone(), fabric.into());
+
+ Ok(())
+ }
+
+ /// Get a reference to the fabric with the specified fabric_id.
+ pub fn get_fabric(&self, id: &FabricId) -> Result<&FabricEntry, FabricConfigError> {
+ self.fabrics
+ .get(id)
+ .ok_or_else(|| FabricConfigError::FabricDoesNotExist(id.to_string()))
+ }
+
+ /// Get a mutable reference to the fabric with the specified fabric_id.
+ pub fn get_fabric_mut(&mut self, id: &FabricId) -> Result<&mut FabricEntry, FabricConfigError> {
+ self.fabrics
+ .get_mut(id)
+ .ok_or_else(|| FabricConfigError::FabricDoesNotExist(id.to_string()))
+ }
+
+ /// Delete a fabric with the specified fabric_id from the [FabricConfig].
+ pub fn delete_fabric(&mut self, id: &FabricId) -> Result<FabricEntry, FabricConfigError> {
+ self.fabrics
+ .remove(id)
+ .ok_or_else(|| FabricConfigError::FabricDoesNotExist(id.to_string()))
+ }
+
+ /// Update the fabric specified by the fabric_id using the [FabricUpdater].
+ pub fn update_fabric(
+ &mut self,
+ id: &FabricId,
+ updater: FabricUpdater,
+ ) -> Result<(), FabricConfigError> {
+ let fabric = self.get_fabric_mut(id)?.fabric_mut();
+
+ match (fabric, updater) {
+ (Fabric::Openfabric(fabric_section), FabricUpdater::Openfabric(updater)) => {
+ let FabricSectionUpdater::<
+ OpenfabricPropertiesUpdater,
+ OpenfabricDeletableProperties,
+ > {
+ ip_prefix,
+ ip6_prefix,
+ properties:
+ OpenfabricPropertiesUpdater {
+ hello_interval,
+ csnp_interval,
+ },
+ delete,
+ } = updater;
+
+ if let Some(prefix) = ip_prefix {
+ fabric_section.ip_prefix = Some(prefix);
+ }
+
+ if let Some(prefix) = ip6_prefix {
+ fabric_section.ip6_prefix = Some(prefix);
+ }
+
+ if let Some(hello_interval) = hello_interval {
+ fabric_section.properties.hello_interval = Some(hello_interval);
+ }
+
+ if let Some(csnp_interval) = csnp_interval {
+ fabric_section.properties.csnp_interval = Some(csnp_interval);
+ }
+
+ for property in delete {
+ match property {
+ FabricDeletableProperties::IpPrefix => {
+ fabric_section.ip_prefix = None;
+ }
+ FabricDeletableProperties::Ip6Prefix => {
+ fabric_section.ip6_prefix = None;
+ }
+ FabricDeletableProperties::Protocol(
+ OpenfabricDeletableProperties::CsnpInterval,
+ ) => fabric_section.properties.csnp_interval = None,
+ FabricDeletableProperties::Protocol(
+ OpenfabricDeletableProperties::HelloInterval,
+ ) => fabric_section.properties.hello_interval = None,
+ }
+ }
+
+ Ok(())
+ }
+ (Fabric::Ospf(fabric_section), FabricUpdater::Ospf(updater)) => {
+ let FabricSectionUpdater::<OspfPropertiesUpdater, OspfDeletableProperties> {
+ ip_prefix,
+ ip6_prefix,
+ properties: OspfPropertiesUpdater { area },
+ delete,
+ } = updater;
+
+ if let Some(prefix) = ip_prefix {
+ fabric_section.ip_prefix = Some(prefix);
+ }
+
+ if let Some(prefix) = ip6_prefix {
+ fabric_section.ip6_prefix = Some(prefix);
+ }
+
+ if let Some(area) = area {
+ fabric_section.properties.area = area;
+ }
+
+ for property in delete {
+ match property {
+ FabricDeletableProperties::IpPrefix => {
+ fabric_section.ip_prefix = None;
+ }
+ FabricDeletableProperties::Ip6Prefix => {
+ fabric_section.ip6_prefix = None;
+ }
+ }
+ }
+
+ Ok(())
+ }
+ _ => Err(FabricConfigError::ProtocolMismatch),
+ }
+ }
+}
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs v3 18/21] common: sdn: fabrics: implement validation
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (21 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 17/21] config: sdn: fabrics: add fabric config Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 19/21] sdn: fabrics: config: add conversion from / to section config Stefan Hanreich
` (51 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
The SDN fabrics configuration needs to validate properties of structs
that are dependent on their context. For instance the IP of a node
needs to be contained in the referenced fabric. Simple schema
validation is not sufficient for proper validation of the complete
fabrics configuration.
In order to better model the validation state via the Rust type
system, we provide a type Valid and a accompanying trait Validatable.
The Valid type can only be constructed from types, that implement the
Validatable trait. Anything wrapped in Valid has to unwrapped before
it can be mutated again, ensuring that only valid values can be
contained in a Valid<T>. This makes it possible for methods to require
callers to validate everything beforehand. This is later utilized by
the FabricConfig to ensure that it is only possible to write validated
configurations to the config file.
We implement Validatable for almost any type representing the fabrics
configuration and call them from the top-level fabric configuration.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/src/common/mod.rs | 2 +
proxmox-ve-config/src/common/valid.rs | 53 ++++++++
proxmox-ve-config/src/sdn/fabric/mod.rs | 126 +++++++++++++++++-
.../src/sdn/fabric/section_config/fabric.rs | 24 +++-
.../src/sdn/fabric/section_config/node.rs | 13 ++
.../section_config/protocol/openfabric.rs | 38 +++++-
.../fabric/section_config/protocol/ospf.rs | 47 ++++++-
7 files changed, 296 insertions(+), 7 deletions(-)
create mode 100644 proxmox-ve-config/src/common/valid.rs
diff --git a/proxmox-ve-config/src/common/mod.rs b/proxmox-ve-config/src/common/mod.rs
index ef09791..9fde536 100644
--- a/proxmox-ve-config/src/common/mod.rs
+++ b/proxmox-ve-config/src/common/mod.rs
@@ -2,6 +2,8 @@ use core::hash::Hash;
use std::cmp::Eq;
use std::collections::HashSet;
+pub mod valid;
+
#[derive(Clone, Debug, Default)]
pub struct Allowlist<T>(HashSet<T>);
diff --git a/proxmox-ve-config/src/common/valid.rs b/proxmox-ve-config/src/common/valid.rs
new file mode 100644
index 0000000..1f92ef9
--- /dev/null
+++ b/proxmox-ve-config/src/common/valid.rs
@@ -0,0 +1,53 @@
+use std::ops::Deref;
+
+/// A wrapper type for validatable structs.
+///
+/// It can only be constructed by implementing the [`Validatable`] type for a struct. Its contents
+/// can be read, but not modified, guaranteeing the content of this struct to always be valid, as
+/// defined by the [`Validatable::validate`] function.
+///
+/// If you want to edit the content, this struct has to be unwrapped via [`Valid<T>::into_inner`].
+#[repr(transparent)]
+#[derive(Clone, Default, Debug)]
+pub struct Valid<T>(T);
+
+impl<T> Valid<T> {
+ /// returns the wrapped value owned, consumes the Valid struct
+ pub fn into_inner(self) -> T {
+ self.0
+ }
+}
+
+impl<T> Deref for Valid<T> {
+ type Target = T;
+
+ fn deref(&self) -> &T {
+ &self.0
+ }
+}
+
+impl<T> AsRef<T> for Valid<T> {
+ fn as_ref(&self) -> &T {
+ &self.0
+ }
+}
+
+/// Defines a struct that can be validated
+///
+/// This can be useful if a struct can not be validated solely by its structure, for instance if
+/// the validity of a value of a field depends on another field. This trait can help with
+/// abstracting that requirement away and implementing it provides the only way of constructing a
+/// [`Valid<T>`].
+pub trait Validatable: Sized {
+ type Error;
+
+ /// Checks whether the values in the struct are valid or not.
+ fn validate(&self) -> Result<(), Self::Error>;
+
+ /// Calls [`Validatable::validate`] to validate the struct and returns a [`Valid<T>`] if
+ /// validation succeeds.
+ fn into_valid(self) -> Result<Valid<Self>, Self::Error> {
+ self.validate()?;
+ Ok(Valid(self))
+ }
+}
diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs
index 3342a70..b7d1f2d 100644
--- a/proxmox-ve-config/src/sdn/fabric/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/mod.rs
@@ -1,11 +1,13 @@
pub mod section_config;
-use std::collections::BTreeMap;
+use std::collections::{BTreeMap, HashSet};
use std::marker::PhantomData;
use std::ops::Deref;
use serde::{Deserialize, Serialize};
+use crate::common::valid::Validatable;
+
use crate::sdn::fabric::section_config::{
fabric::{
Fabric, FabricDeletableProperties, FabricId, FabricSection, FabricSectionUpdater,
@@ -34,12 +36,26 @@ pub enum FabricConfigError {
FabricDoesNotExist(String),
#[error("node '{0}' does not exist in fabric '{1}'")]
NodeDoesNotExist(String, String),
+ #[error("node IP {0} is outside the IP prefix {1} of the fabric")]
+ NodeIpOutsideFabricRange(String, String),
#[error("node has a different protocol than the referenced fabric")]
ProtocolMismatch,
#[error("fabric '{0}' already exists in config")]
DuplicateFabric(String),
#[error("node '{0}' already exists in config for fabric {1}")]
DuplicateNode(String, String),
+ #[error("fabric {0} contains nodes with duplicated IPs")]
+ DuplicateNodeIp(String),
+ #[error("fabric '{0}' does not have an IP prefix configured for the node IP {1}")]
+ FabricNoIpPrefixForNode(String, String),
+ #[error("fabric '{0}' does not have an IP prefix configured")]
+ FabricNoIpPrefix(String),
+ #[error("node '{0}' does not have an IP configured")]
+ NodeNoIp(String),
+ #[error("interface is already in use by another fabric")]
+ DuplicateInterface,
+ #[error("IPv6 is currently not supported for protocol {0}")]
+ NoIpv6(String),
// should usually not occur, but we still check for it nonetheless
#[error("mismatched fabric_id")]
FabricIdMismatch,
@@ -367,6 +383,75 @@ impl From<Fabric> for FabricEntry {
}
}
+impl Validatable for FabricEntry {
+ type Error = FabricConfigError;
+
+ /// Validates the [FabricEntry] configuration.
+ ///
+ /// Ensures that:
+ /// - Node IP addresses are within their respective fabric IP prefix ranges
+ /// - IP addresses are unique across all nodes in the fabric
+ /// - Each node passes its own validation checks
+ fn validate(&self) -> Result<(), FabricConfigError> {
+ let fabric = self.fabric();
+
+ let mut ips = HashSet::new();
+ let mut ip6s = HashSet::new();
+
+ for (_id, node) in self.nodes() {
+ if let Some(ip) = node.ip() {
+ // Fabric needs to have an IPv4 prefix if a node has an IPv4 configured
+ let prefix = fabric.ip_prefix().ok_or_else(|| {
+ FabricConfigError::FabricNoIpPrefixForNode(
+ fabric.id().to_string(),
+ ip.to_string(),
+ )
+ })?;
+
+ // Fabric prefix needs to contain the node IP
+ if !prefix.contains_address(&ip) {
+ return Err(FabricConfigError::NodeIpOutsideFabricRange(
+ ip.to_string(),
+ prefix.to_string(),
+ ));
+ }
+ }
+
+ if let Some(ip) = node.ip6() {
+ // Fabric needs to have an IPv6 prefix if a node has an IPv6 configured
+ let prefix = fabric.ip6_prefix().ok_or_else(|| {
+ FabricConfigError::FabricNoIpPrefixForNode(
+ fabric.id().to_string(),
+ ip.to_string(),
+ )
+ })?;
+
+ // Fabric prefix needs to contain the node IP
+ if !prefix.contains_address(&ip) {
+ return Err(FabricConfigError::NodeIpOutsideFabricRange(
+ ip.to_string(),
+ prefix.to_string(),
+ ));
+ }
+ }
+
+ // Node IPs need to be unique inside a fabric
+ if !node.ip().map(|ip| ips.insert(ip)).unwrap_or(true) {
+ return Err(FabricConfigError::DuplicateNodeIp(fabric.id().to_string()));
+ }
+
+ // Node IPs need to be unique inside a fabric
+ if !node.ip6().map(|ip| ip6s.insert(ip)).unwrap_or(true) {
+ return Err(FabricConfigError::DuplicateNodeIp(fabric.id().to_string()));
+ }
+
+ node.validate()?;
+ }
+
+ fabric.validate()
+ }
+}
+
/// A complete SDN fabric configuration.
///
/// This struct contains the whole fabric configuration in a tree-like structure (fabrics -> nodes
@@ -384,6 +469,45 @@ impl Deref for FabricConfig {
}
}
+impl Validatable for FabricConfig {
+ type Error = FabricConfigError;
+
+ /// Validate the [FabricConfig].
+ ///
+ /// Ensures that:
+ /// - (node, interface) combinations exist only once across all fabrics
+ /// - every entry (fabric) validates
+ fn validate(&self) -> Result<(), FabricConfigError> {
+ let mut node_interfaces = HashSet::new();
+
+ // validate that each (node, interface) combination exists only once across all fabrics
+ for entry in self.fabrics.values() {
+ for (node_id, node) in entry.nodes() {
+ match node {
+ Node::Ospf(node_section) => {
+ if !node_section.properties().interfaces().all(|interface| {
+ node_interfaces.insert((node_id, interface.name.as_str()))
+ }) {
+ return Err(FabricConfigError::DuplicateInterface);
+ }
+ }
+ Node::Openfabric(node_section) => {
+ if !node_section.properties().interfaces().all(|interface| {
+ node_interfaces.insert((node_id, interface.name.as_str()))
+ }) {
+ return Err(FabricConfigError::DuplicateInterface);
+ }
+ }
+ }
+ }
+
+ entry.validate()?;
+ }
+
+ Ok(())
+ }
+}
+
impl FabricConfig {
/// Add a fabric to the [FabricConfig].
///
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
index 75a3093..b8d7649 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
@@ -7,11 +7,15 @@ use proxmox_schema::{
Updater, UpdaterType,
};
-use crate::sdn::fabric::section_config::protocol::{
- openfabric::{
- OpenfabricDeletableProperties, OpenfabricProperties, OpenfabricPropertiesUpdater,
+use crate::common::valid::Validatable;
+use crate::sdn::fabric::{
+ section_config::protocol::{
+ openfabric::{
+ OpenfabricDeletableProperties, OpenfabricProperties, OpenfabricPropertiesUpdater,
+ },
+ ospf::{OspfDeletableProperties, OspfProperties, OspfPropertiesUpdater},
},
- ospf::{OspfDeletableProperties, OspfProperties, OspfPropertiesUpdater},
+ FabricConfigError,
};
pub const FABRIC_ID_REGEX_STR: &str = r"(?:[a-zA-Z0-9])(?:[a-zA-Z0-9\-]){0,6}(?:[a-zA-Z0-9])?";
@@ -195,6 +199,18 @@ impl Fabric {
}
}
+impl Validatable for Fabric {
+ type Error = FabricConfigError;
+
+ /// Validate the [Fabric] by calling the validation function for the respective protocol.
+ fn validate(&self) -> Result<(), Self::Error> {
+ match self {
+ Fabric::Openfabric(fabric_section) => fabric_section.validate(),
+ Fabric::Ospf(fabric_section) => fabric_section.validate(),
+ }
+ }
+}
+
impl From<FabricSection<OpenfabricProperties>> for Fabric {
fn from(section: FabricSection<OpenfabricProperties>) -> Self {
Fabric::Openfabric(section)
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
index b1b2034..4dca1c9 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
@@ -10,10 +10,12 @@ use proxmox_schema::{
StringSchema, UpdaterType,
};
+use crate::common::valid::Validatable;
use crate::sdn::fabric::section_config::{
fabric::{FabricId, FABRIC_ID_REGEX_STR},
protocol::{openfabric::OpenfabricNodeProperties, ospf::OspfNodeProperties},
};
+use crate::sdn::fabric::FabricConfigError;
pub const NODE_ID_REGEX_STR: &str = r"(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]){0,61}(?:[a-zA-Z0-9]){0,1})";
@@ -212,6 +214,17 @@ impl Node {
}
}
+impl Validatable for Node {
+ type Error = FabricConfigError;
+
+ fn validate(&self) -> Result<(), Self::Error> {
+ match self {
+ Node::Openfabric(node_section) => node_section.validate(),
+ Node::Ospf(node_section) => node_section.validate(),
+ }
+ }
+}
+
impl From<NodeSection<OpenfabricNodeProperties>> for Node {
fn from(value: NodeSection<OpenfabricNodeProperties>) -> Self {
Self::Openfabric(value)
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/openfabric.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/openfabric.rs
index 156ff2b..ccbde63 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/openfabric.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/openfabric.rs
@@ -6,7 +6,13 @@ use serde::{Deserialize, Serialize};
use proxmox_schema::{api, property_string::PropertyString, ApiStringFormat, Updater};
use proxmox_sdn_types::openfabric::{CsnpInterval, HelloInterval, HelloMultiplier};
-use crate::sdn::fabric::section_config::interface::InterfaceName;
+use crate::{
+ common::valid::Validatable,
+ sdn::fabric::{
+ section_config::{fabric::FabricSection, interface::InterfaceName, node::NodeSection},
+ FabricConfigError,
+ },
+};
/// Protocol-specific options for an OpenFabric Fabric.
#[api]
@@ -24,6 +30,21 @@ pub struct OpenfabricProperties {
pub(crate) csnp_interval: Option<CsnpInterval>,
}
+impl Validatable for FabricSection<OpenfabricProperties> {
+ type Error = FabricConfigError;
+
+ /// Validates the [FabricSection<OpenfabricProperties>].
+ ///
+ /// Checks if we have either IPv4-prefix or IPv6-prefix. If both are not set, return an error.
+ fn validate(&self) -> Result<(), Self::Error> {
+ if self.ip_prefix().is_none() && self.ip6_prefix().is_none() {
+ return Err(FabricConfigError::FabricNoIpPrefix(self.id().to_string()));
+ }
+
+ Ok(())
+ }
+}
+
#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
#[serde(rename_all = "snake_case")]
pub enum OpenfabricDeletableProperties {
@@ -61,6 +82,21 @@ impl OpenfabricNodeProperties {
}
}
+impl Validatable for NodeSection<OpenfabricNodeProperties> {
+ type Error = FabricConfigError;
+
+ /// Validates the [FabricSection<OpenfabricProperties>].
+ ///
+ /// Checks if we have either an IPv4 or an IPv6 address. If neither is set, return an error.
+ fn validate(&self) -> Result<(), Self::Error> {
+ if self.ip().is_none() && self.ip6().is_none() {
+ return Err(FabricConfigError::NodeNoIp(self.id().to_string()));
+ }
+
+ Ok(())
+ }
+}
+
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OpenfabricNodeDeletableProperties {
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/ospf.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/ospf.rs
index 4c368fa..f8010a3 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/ospf.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/ospf.rs
@@ -6,7 +6,13 @@ use serde::{Deserialize, Serialize};
use proxmox_schema::{api, property_string::PropertyString, ApiStringFormat, Updater};
-use crate::sdn::fabric::section_config::interface::InterfaceName;
+use crate::{
+ common::valid::Validatable,
+ sdn::fabric::{
+ section_config::{fabric::FabricSection, interface::InterfaceName, node::NodeSection},
+ FabricConfigError,
+ },
+};
#[api]
#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)]
@@ -22,6 +28,26 @@ impl OspfProperties {
}
}
+impl Validatable for FabricSection<OspfProperties> {
+ type Error = FabricConfigError;
+
+ /// Validate the [FabricSection<OspfProperties>].
+ ///
+ /// Checks if the ip-prefix (IPv4) is set. If not, then return an error.
+ /// If the ip6-prefix (IPv6) is set, also return an error, as OSPF doesn't support IPv6.
+ fn validate(&self) -> Result<(), Self::Error> {
+ if self.ip_prefix().is_none() {
+ return Err(FabricConfigError::FabricNoIpPrefix(self.id().to_string()));
+ }
+
+ if self.ip6_prefix().is_some() {
+ return Err(FabricConfigError::NoIpv6("ospf".to_string()));
+ }
+
+ Ok(())
+ }
+}
+
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", untagged)]
pub enum OspfDeletableProperties {}
@@ -55,6 +81,25 @@ impl OspfNodeProperties {
}
}
+impl Validatable for NodeSection<OspfNodeProperties> {
+ type Error = FabricConfigError;
+
+ /// Validate the [NodeSection<OspfNodeProperties>].
+ ///
+ /// Error if the IPv4 address is not set. Error if the IPv6 address is set (OSPF does not
+ /// support IPv6).
+ fn validate(&self) -> Result<(), Self::Error> {
+ if self.ip().is_none() {
+ return Err(FabricConfigError::NodeNoIp(self.id().to_string()));
+ }
+ if self.ip6().is_some() {
+ return Err(FabricConfigError::NoIpv6("ospf".to_string()));
+ }
+
+ Ok(())
+ }
+}
+
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", untagged)]
pub enum OspfNodeDeletableProperties {
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs v3 19/21] sdn: fabrics: config: add conversion from / to section config
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (22 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 18/21] common: sdn: fabrics: implement validation Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 20/21] sdn: fabrics: implement FRR configuration generation Stefan Hanreich
` (50 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
Add helper methods for populating a FabricConfig from the section
config, as well as converting it back into the respective section
config structs.
By utilizing the Valid type, we can ensure that only valid
configurations get written via the provided write_section_config
method, since it is only implemented for Valid<FabricConfig>. Because
validation can be expensive, particularly when doing multiple changes
to the fabric config, we only validate the config directly before
converting it into the section config.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/src/sdn/fabric/mod.rs | 91 ++++++++++++++++++++++++-
1 file changed, 90 insertions(+), 1 deletion(-)
diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs
index b7d1f2d..57e2e6f 100644
--- a/proxmox-ve-config/src/sdn/fabric/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/mod.rs
@@ -4,9 +4,12 @@ use std::collections::{BTreeMap, HashSet};
use std::marker::PhantomData;
use std::ops::Deref;
+use anyhow::Error;
use serde::{Deserialize, Serialize};
-use crate::common::valid::Validatable;
+use proxmox_section_config::typed::{ApiSectionDataEntry, SectionConfigData};
+
+use crate::common::valid::{Valid, Validatable};
use crate::sdn::fabric::section_config::{
fabric::{
@@ -28,6 +31,7 @@ use crate::sdn::fabric::section_config::{
OspfNodePropertiesUpdater, OspfProperties, OspfPropertiesUpdater,
},
},
+ Either, Section,
};
#[derive(thiserror::Error, Debug)]
@@ -638,4 +642,89 @@ impl FabricConfig {
_ => Err(FabricConfigError::ProtocolMismatch),
}
}
+
+ /// Constructs a valid [FabricConfig] from section-config data.
+ ///
+ /// Iterates through the [SectionConfigData<Section>] and matches on the [Section] enum. Then
+ /// construct the [FabricConfig] and validate it.
+ pub fn from_section_config(
+ config: SectionConfigData<Section>,
+ ) -> Result<Valid<Self>, FabricConfigError> {
+ let mut fabrics = BTreeMap::new();
+ let mut nodes = Vec::new();
+
+ for (_id, section) in config {
+ let fabric_or_node = Either::from(section);
+
+ match fabric_or_node {
+ Either::Left(fabric) => {
+ fabrics.insert(fabric.id().clone(), FabricEntry::from(fabric));
+ }
+ Either::Right(node) => {
+ nodes.push(node);
+ }
+ };
+ }
+
+ for node in nodes {
+ fabrics
+ .get_mut(node.id().fabric_id())
+ .ok_or_else(|| {
+ FabricConfigError::FabricDoesNotExist(node.id().fabric_id().to_string())
+ })?
+ .add_node(node)?;
+ }
+
+ let config = Self { fabrics };
+ config.into_valid()
+ }
+
+ /// Constructs a valid [FabricConfig] from the raw section-config file content.
+ ///
+ /// This will call the [Section::parse_section_config] function to parse the raw string into a
+ /// [SectionConfigData<Section>] struct. Then construct the valid [FabricConfig] with
+ /// [Self::from_section_config].
+ pub fn parse_section_config(config: &str) -> Result<Valid<Self>, Error> {
+ let data = Section::parse_section_config("fabrics.cfg", config)?;
+ Self::from_section_config(data).map_err(anyhow::Error::msg)
+ }
+
+ /// Validate [FabricConfig] and write the raw config to a String.
+ ///
+ /// Validates the config and calls [Valid<FabricConfig>::write_section_config].
+ pub fn write_section_config(&self) -> Result<String, Error> {
+ self.clone().into_valid()?.write_section_config()
+ }
+}
+
+impl Valid<FabricConfig> {
+ /// Converts a valid [FabricConfig] into a [SectionConfigData<Section>].
+ ///
+ /// This function is implemented on [Valid<FabricConfig>], ensuring that only a valid
+ /// [FabricConfig] can be written to the file.
+ pub fn into_section_config(self) -> SectionConfigData<Section> {
+ let config = self.into_inner();
+
+ let mut section_config = SectionConfigData::default();
+
+ for (fabric_id, fabric_entry) in config.fabrics {
+ let (fabric, fabric_nodes) = fabric_entry.into_section_config();
+
+ section_config.insert(fabric_id.to_string(), Section::from(fabric));
+
+ for node in fabric_nodes {
+ section_config.insert(node.id().to_string(), Section::from(node));
+ }
+ }
+
+ section_config
+ }
+
+ /// Consumes the [Valid<FabricConfig>] and writes the raw section-config content to a String.
+ ///
+ /// This function is implemented on [Valid<FabricConfig>], ensuring that only a valid
+ /// [FabricConfig] can be written to the file.
+ pub fn write_section_config(self) -> Result<String, Error> {
+ Section::write_section_config("fabrics.cfg", &self.into_section_config())
+ }
}
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs v3 20/21] sdn: fabrics: implement FRR configuration generation
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (23 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 19/21] sdn: fabrics: config: add conversion from / to section config Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 21/21] ve-config: add integrations tests Stefan Hanreich
` (49 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
Add support to generate FRR configurations (using proxmox-frr types)
from a valid FabricConfig. Introduce a feature flag for an optional
dependency on proxmox-frr for the FRR types. Add the FrrConfigBuild
struct, which currently holds a Valid<FabricConfig> but will
eventually contain all configurations that output an FRR config. This
ensures all the different configurations are combined in one struct
and converted into a single valid FRR config.
When converting to FRR config types, iterate over the fabrics to find
the current node (retrieved from the Perl function invocation). Match
the protocol and generate the FRR configuration for this node and all
its interfaces. OpenFabric supports IPv4 and IPv6, requiring special
cases in the interface configuration (e.g., 'ip6 router...' vs. 'ip
router...'). OpenFabric can also operate in dual-stack mode using both
IPv6 and IPv4.
Currently, the FRR configuration includes five object types: routers,
interfaces, access-lists, route-maps, and `ip protocol` statements.
Routers define the fabric and its fabric-wide properties, while
interfaces specify their fabric membership and other fabric-specific
properties.
Access-lists, route-maps, and `ip protocol` statements, though not
strictly necessary, allow us to create better routes. They edit the
learned routes so that they remap the source address of outgoing
packets. Access-lists create a list of prefixes matched by route-maps,
which then rewrite the source address. `ip protocol` statements add
the route-map to the protocol daemon, ensuring all routes from that
protocol are matched.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
Cargo.toml | 1 +
proxmox-ve-config/Cargo.toml | 5 +
proxmox-ve-config/debian/control | 23 +-
proxmox-ve-config/src/sdn/fabric/frr.rs | 390 ++++++++++++++++++++++++
proxmox-ve-config/src/sdn/fabric/mod.rs | 2 +
proxmox-ve-config/src/sdn/frr.rs | 42 +++
proxmox-ve-config/src/sdn/mod.rs | 2 +
7 files changed, 463 insertions(+), 2 deletions(-)
create mode 100644 proxmox-ve-config/src/sdn/fabric/frr.rs
create mode 100644 proxmox-ve-config/src/sdn/frr.rs
diff --git a/Cargo.toml b/Cargo.toml
index e51fb59..29b7875 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,6 +25,7 @@ serde = { version = "1" }
serde_with = "3"
thiserror = "1.0.59"
+proxmox-frr = { version = "0.1", path = "proxmox-frr" }
proxmox-network-types = { version = "0.1" }
proxmox-schema = { version = "4" }
proxmox-sdn-types = { version = "0.1", path = "proxmox-sdn-types" }
diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml
index c5aeba9..cea541e 100644
--- a/proxmox-ve-config/Cargo.toml
+++ b/proxmox-ve-config/Cargo.toml
@@ -13,6 +13,7 @@ nix = "0.26"
regex = { workspace = true }
const_format = { workspace = true }
thiserror = { workspace = true }
+tracing = "0.1.37"
serde = { workspace = true, features = [ "derive" ] }
serde_json = "1"
@@ -20,9 +21,13 @@ serde_plain = "1"
serde_with = { workspace = true }
proxmox-serde = { version = "0.1.2", features = [ "perl" ]}
+proxmox-frr = { workspace = true, optional = true }
proxmox-network-types = { workspace = true, features = [ "api-types" ] }
proxmox-schema = { workspace = true, features = [ "api-types" ] }
proxmox-sdn-types = { workspace = true }
proxmox-section-config = { version = "3" }
proxmox-sys = "0.6.4"
proxmox-sortable-macro = "0.1.3"
+
+[features]
+frr = ["dep:proxmox-frr"]
diff --git a/proxmox-ve-config/debian/control b/proxmox-ve-config/debian/control
index 57d7987..807e9cd 100644
--- a/proxmox-ve-config/debian/control
+++ b/proxmox-ve-config/debian/control
@@ -26,7 +26,8 @@ Build-Depends-Arch: cargo:native <!nocheck>,
librust-serde-json-1+default-dev <!nocheck>,
librust-serde-plain-1+default-dev <!nocheck>,
librust-serde-with-3+default-dev <!nocheck>,
- librust-thiserror-1+default-dev (>= 1.0.59-~~) <!nocheck>
+ librust-thiserror-1+default-dev (>= 1.0.59-~~) <!nocheck>,
+ librust-tracing-0.1+default-dev (>= 0.1.37-~~) <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
Standards-Version: 4.7.0
Vcs-Git: git://git.proxmox.com/git/proxmox-ve-rs.git
@@ -59,7 +60,10 @@ Depends:
librust-serde-json-1+default-dev,
librust-serde-plain-1+default-dev,
librust-serde-with-3+default-dev,
- librust-thiserror-1+default-dev (>= 1.0.59-~~)
+ librust-thiserror-1+default-dev (>= 1.0.59-~~),
+ librust-tracing-0.1+default-dev (>= 0.1.37-~~)
+Suggests:
+ librust-proxmox-ve-config+frr-dev (= ${binary:Version})
Provides:
librust-proxmox-ve-config+default-dev (= ${binary:Version}),
librust-proxmox-ve-config-0-dev (= ${binary:Version}),
@@ -70,3 +74,18 @@ Provides:
librust-proxmox-ve-config-0.2.3+default-dev (= ${binary:Version})
Description: Rust crate "proxmox-ve-config" - Rust source code
Source code for Debianized Rust crate "proxmox-ve-config"
+
+Package: librust-proxmox-ve-config+frr-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-proxmox-ve-config-dev (= ${binary:Version}),
+ librust-proxmox-frr-0.1+default-dev
+Provides:
+ librust-proxmox-ve-config-0+frr-dev (= ${binary:Version}),
+ librust-proxmox-ve-config-0.2+frr-dev (= ${binary:Version}),
+ librust-proxmox-ve-config-0.2.3+frr-dev (= ${binary:Version})
+Description: Rust crate "proxmox-ve-config" - feature "frr"
+ This metapackage enables feature "frr" for the Rust proxmox-ve-config crate, by
+ pulling in any additional dependencies needed by that feature.
diff --git a/proxmox-ve-config/src/sdn/fabric/frr.rs b/proxmox-ve-config/src/sdn/fabric/frr.rs
new file mode 100644
index 0000000..1857956
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/frr.rs
@@ -0,0 +1,390 @@
+use std::net::{IpAddr, Ipv4Addr};
+use tracing;
+
+use proxmox_frr::{
+ ospf::{self, NetworkType},
+ route_map::{
+ AccessAction, AccessList, AccessListName, AccessListRule, ProtocolRouteMap, ProtocolType,
+ RouteMap, RouteMapMatch, RouteMapMatchInner, RouteMapName, RouteMapSet,
+ },
+ FrrConfig, FrrWord, Interface, InterfaceName, Router, RouterName,
+};
+use proxmox_network_types::ip_address::Cidr;
+use proxmox_sdn_types::net::Net;
+
+use crate::common::valid::Valid;
+
+use crate::sdn::fabric::{
+ section_config::{
+ fabric::FabricId,
+ node::NodeId,
+ protocol::{
+ openfabric::{OpenfabricInterfaceProperties, OpenfabricProperties},
+ ospf::OspfInterfaceProperties,
+ },
+ },
+ FabricConfig, FabricEntry,
+};
+
+/// Constructs the FRR config from the the passed [Valid<FabricConfig>].
+///
+/// Iterates over the [FabricConfig] and constructs all the FRR routers, interfaces, route-maps,
+/// etc. which area all appended to the passed [FrrConfig].
+pub fn build_fabric(
+ current_node: NodeId,
+ config: Valid<FabricConfig>,
+ frr_config: &mut FrrConfig,
+) -> Result<(), anyhow::Error> {
+ let mut routemap_seq = 100;
+ let mut current_router_id: Option<Ipv4Addr> = None;
+ let mut current_net: Option<Net> = None;
+
+ for (fabric_id, entry) in config.into_inner().iter() {
+ match entry {
+ FabricEntry::Openfabric(openfabric_entry) => {
+ // Get the current node of this fabric, if it doesn't exist, skip this fabric and
+ // don't generate any FRR config.
+ let Ok(node) = openfabric_entry.node_section(¤t_node) else {
+ continue;
+ };
+
+ if current_net.is_none() {
+ current_net = match (node.ip(), node.ip6()) {
+ (Some(ip), _) => Some(ip.into()),
+ (_, Some(ip6)) => Some(ip6.into()),
+ (_, _) => None,
+ }
+ }
+
+ let net = current_net
+ .as_ref()
+ .ok_or_else(|| anyhow::anyhow!("no IPv4 or IPv6 set for node"))?;
+ let (router_name, router_item) = build_openfabric_router(fabric_id, net.clone())?;
+ frr_config.router.insert(router_name, router_item);
+
+ // Create dummy interface for fabric
+ let (interface, interface_name) = build_openfabric_dummy_interface(
+ fabric_id,
+ node.ip().is_some(),
+ node.ip6().is_some(),
+ )?;
+
+ if frr_config
+ .interfaces
+ .insert(interface_name, interface)
+ .is_some()
+ {
+ tracing::error!(
+ "An interface with the same name as the dummy interface exists"
+ );
+ }
+
+ let fabric = openfabric_entry.fabric_section();
+
+ for interface in node.properties().interfaces.iter() {
+ let (interface, interface_name) = build_openfabric_interface(
+ fabric_id,
+ interface,
+ fabric.properties(),
+ node.ip().is_some(),
+ node.ip6().is_some(),
+ )?;
+
+ if frr_config
+ .interfaces
+ .insert(interface_name, interface)
+ .is_some()
+ {
+ tracing::warn!("An interface cannot be in multiple openfabric fabrics");
+ }
+ }
+
+ if let Some(ipv4cidr) = fabric.ip_prefix() {
+ let rule = AccessListRule {
+ action: AccessAction::Permit,
+ network: Cidr::from(ipv4cidr),
+ seq: None,
+ };
+ let access_list_name =
+ AccessListName::new(format!("pve_openfabric_{}_ips", fabric_id));
+ frr_config.access_lists.push(AccessList {
+ name: access_list_name,
+ rules: vec![rule],
+ });
+ }
+ if let Some(ipv6cidr) = fabric.ip6_prefix() {
+ let rule = AccessListRule {
+ action: AccessAction::Permit,
+ network: Cidr::from(ipv6cidr),
+ seq: None,
+ };
+ let access_list_name =
+ AccessListName::new(format!("pve_openfabric_{}_ip6s", fabric_id));
+ frr_config.access_lists.push(AccessList {
+ name: access_list_name,
+ rules: vec![rule],
+ });
+ }
+
+ if let Some(ipv4) = node.ip() {
+ // create route-map
+ frr_config.routemaps.push(build_openfabric_routemap(
+ fabric_id,
+ IpAddr::V4(ipv4),
+ routemap_seq,
+ ));
+ routemap_seq += 10;
+
+ let protocol_routemap = ProtocolRouteMap {
+ is_ipv6: false,
+ protocol: ProtocolType::Openfabric,
+ routemap_name: RouteMapName::new("pve_openfabric".to_owned()),
+ };
+
+ frr_config.protocol_routemaps.insert(protocol_routemap);
+ }
+ if let Some(ipv6) = node.ip6() {
+ // create route-map
+ frr_config.routemaps.push(build_openfabric_routemap(
+ fabric_id,
+ IpAddr::V6(ipv6),
+ routemap_seq,
+ ));
+ routemap_seq += 10;
+
+ let protocol_routemap = ProtocolRouteMap {
+ is_ipv6: true,
+ protocol: ProtocolType::Openfabric,
+ routemap_name: RouteMapName::new("pve_openfabric6".to_owned()),
+ };
+
+ frr_config.protocol_routemaps.insert(protocol_routemap);
+ }
+ }
+ FabricEntry::Ospf(ospf_entry) => {
+ let Ok(node) = ospf_entry.node_section(¤t_node) else {
+ continue;
+ };
+
+ let router_id = current_router_id
+ .get_or_insert(node.ip().expect("node must have an ipv4 address"));
+
+ let fabric = ospf_entry.fabric_section();
+
+ let frr_word_area = FrrWord::new(fabric.properties().area.to_string())?;
+ let frr_area = ospf::Area::new(frr_word_area)?;
+ let (router_name, router_item) = build_ospf_router(*router_id)?;
+ frr_config.router.insert(router_name, router_item);
+
+ // Add dummy interface
+ let (interface, interface_name) =
+ build_ospf_dummy_interface(fabric_id, frr_area.clone())?;
+
+ if frr_config
+ .interfaces
+ .insert(interface_name, interface)
+ .is_some()
+ {
+ tracing::error!(
+ "An interface with the same name as the dummy interface exists"
+ );
+ }
+
+ for interface in node.properties().interfaces.iter() {
+ let (interface, interface_name) =
+ build_ospf_interface(frr_area.clone(), interface)?;
+
+ if frr_config
+ .interfaces
+ .insert(interface_name, interface)
+ .is_some()
+ {
+ tracing::warn!("An interface cannot be in multiple openfabric fabrics");
+ }
+ }
+
+ let access_list_name = AccessListName::new(format!("pve_ospf_{}_ips", fabric_id));
+
+ let rule = AccessListRule {
+ action: AccessAction::Permit,
+ network: Cidr::from(
+ fabric.ip_prefix().expect("fabric must have a ipv4 prefix"),
+ ),
+ seq: None,
+ };
+
+ frr_config.access_lists.push(AccessList {
+ name: access_list_name,
+ rules: vec![rule],
+ });
+
+ let routemap = build_ospf_dummy_routemap(
+ fabric_id,
+ node.ip().expect("node must have an ipv4 address"),
+ routemap_seq,
+ )?;
+
+ routemap_seq += 10;
+ frr_config.routemaps.push(routemap);
+
+ let protocol_routemap = ProtocolRouteMap {
+ is_ipv6: false,
+ protocol: ProtocolType::Ospf,
+ routemap_name: RouteMapName::new("pve_ospf".to_owned()),
+ };
+
+ frr_config.protocol_routemaps.insert(protocol_routemap);
+ }
+ }
+ }
+ Ok(())
+}
+
+/// Helper that builds a OSPF router with a the router_id.
+fn build_ospf_router(router_id: Ipv4Addr) -> Result<(RouterName, Router), anyhow::Error> {
+ let ospf_router = proxmox_frr::ospf::OspfRouter { router_id };
+ let router_item = Router::Ospf(ospf_router);
+ let router_name = RouterName::Ospf(proxmox_frr::ospf::OspfRouterName);
+ Ok((router_name, router_item))
+}
+
+/// Helper that builds a OpenFabric router from a fabric_id and a [Net].
+fn build_openfabric_router(
+ fabric_id: &FabricId,
+ net: Net,
+) -> Result<(RouterName, Router), anyhow::Error> {
+ let ofr = proxmox_frr::openfabric::OpenfabricRouter { net };
+ let router_item = Router::Openfabric(ofr);
+ let frr_word_id = FrrWord::new(fabric_id.to_string())?;
+ let router_name = RouterName::Openfabric(frr_word_id.into());
+ Ok((router_name, router_item))
+}
+
+/// Helper that builds a OSPF interface from an [ospf::Area] and the [OspfInterfaceProperties].
+fn build_ospf_interface(
+ area: ospf::Area,
+ interface: &OspfInterfaceProperties,
+) -> Result<(Interface, InterfaceName), anyhow::Error> {
+ let frr_interface = proxmox_frr::ospf::OspfInterface {
+ area,
+ // Interfaces are always none-passive
+ passive: None,
+ network_type: if interface.ip.is_some() {
+ None
+ } else {
+ Some(NetworkType::PointToPoint)
+ },
+ };
+
+ let interface_name = InterfaceName::Ospf(interface.name.parse()?);
+ Ok((frr_interface.into(), interface_name))
+}
+
+/// Helper that builds the OSPF dummy interface using the [FabricId] and the [ospf::Area].
+fn build_ospf_dummy_interface(
+ fabric_id: &FabricId,
+ area: ospf::Area,
+) -> Result<(Interface, InterfaceName), anyhow::Error> {
+ let frr_interface = proxmox_frr::ospf::OspfInterface {
+ area,
+ passive: Some(true),
+ network_type: None,
+ };
+ let interface_name = InterfaceName::Openfabric(format!("dummy_{}", fabric_id).parse()?);
+ Ok((frr_interface.into(), interface_name))
+}
+
+/// Helper that builds the OpenFabric interface.
+///
+/// Takes the [FabricId], [OpenfabricInterfaceProperties], [OpenfabricProperties] and flags for
+/// ipv4 and ipv6.
+fn build_openfabric_interface(
+ fabric_id: &FabricId,
+ interface: &OpenfabricInterfaceProperties,
+ fabric_config: &OpenfabricProperties,
+ is_ipv4: bool,
+ is_ipv6: bool,
+) -> Result<(Interface, InterfaceName), anyhow::Error> {
+ let frr_word = FrrWord::new(fabric_id.to_string())?;
+ let mut frr_interface = proxmox_frr::openfabric::OpenfabricInterface {
+ fabric_id: frr_word.into(),
+ // Every interface is not passive by default
+ passive: None,
+ // Get properties from fabric
+ hello_interval: fabric_config.hello_interval,
+ csnp_interval: fabric_config.csnp_interval,
+ hello_multiplier: interface.hello_multiplier,
+ is_ipv4,
+ is_ipv6,
+ };
+ // If no specific hello_interval is set, get default one from fabric
+ // config
+ if frr_interface.hello_interval().is_none() {
+ frr_interface.set_hello_interval(fabric_config.hello_interval);
+ }
+ let interface_name = InterfaceName::Openfabric(interface.name.parse()?);
+ Ok((frr_interface.into(), interface_name))
+}
+
+/// Helper that builds a OpenFabric interface using a [FabricId] and ipv4/6 flags.
+fn build_openfabric_dummy_interface(
+ fabric_id: &FabricId,
+ is_ipv4: bool,
+ is_ipv6: bool,
+) -> Result<(Interface, InterfaceName), anyhow::Error> {
+ let frr_word = FrrWord::new(fabric_id.to_string())?;
+ let frr_interface = proxmox_frr::openfabric::OpenfabricInterface {
+ fabric_id: frr_word.into(),
+ hello_interval: None,
+ passive: Some(true),
+ csnp_interval: None,
+ hello_multiplier: None,
+ is_ipv4,
+ is_ipv6,
+ };
+ let interface_name = InterfaceName::Openfabric(format!("dummy_{}", fabric_id).parse()?);
+ Ok((frr_interface.into(), interface_name))
+}
+
+/// Helper that builds a RouteMap for the OpenFabric protocol.
+fn build_openfabric_routemap(fabric_id: &FabricId, router_ip: IpAddr, seq: u32) -> RouteMap {
+ let routemap_name = match router_ip {
+ IpAddr::V4(_) => RouteMapName::new("pve_openfabric".to_owned()),
+ IpAddr::V6(_) => RouteMapName::new("pve_openfabric6".to_owned()),
+ };
+ RouteMap {
+ name: routemap_name.clone(),
+ seq,
+ action: AccessAction::Permit,
+ matches: vec![match router_ip {
+ IpAddr::V4(_) => RouteMapMatch::V4(RouteMapMatchInner::IpAddress(AccessListName::new(
+ format!("pve_openfabric_{fabric_id}_ips"),
+ ))),
+ IpAddr::V6(_) => RouteMapMatch::V6(RouteMapMatchInner::IpAddress(AccessListName::new(
+ format!("pve_openfabric_{fabric_id}_ip6s"),
+ ))),
+ }],
+ sets: vec![RouteMapSet::IpSrc(router_ip)],
+ }
+}
+
+/// Helper that builds a RouteMap for the OSPF protocol.
+fn build_ospf_dummy_routemap(
+ fabric_id: &FabricId,
+ router_ip: Ipv4Addr,
+ seq: u32,
+) -> Result<RouteMap, anyhow::Error> {
+ let routemap_name = RouteMapName::new("pve_ospf".to_owned());
+ // create route-map
+ let routemap = RouteMap {
+ name: routemap_name.clone(),
+ seq,
+ action: AccessAction::Permit,
+ matches: vec![RouteMapMatch::V4(RouteMapMatchInner::IpAddress(
+ AccessListName::new(format!("pve_ospf_{fabric_id}_ips")),
+ ))],
+ sets: vec![RouteMapSet::IpSrc(IpAddr::from(router_ip))],
+ };
+
+ Ok(routemap)
+}
diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs
index 57e2e6f..9cd9e37 100644
--- a/proxmox-ve-config/src/sdn/fabric/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/mod.rs
@@ -1,3 +1,5 @@
+#[cfg(feature = "frr")]
+pub mod frr;
pub mod section_config;
use std::collections::{BTreeMap, HashSet};
diff --git a/proxmox-ve-config/src/sdn/frr.rs b/proxmox-ve-config/src/sdn/frr.rs
new file mode 100644
index 0000000..f7929c1
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/frr.rs
@@ -0,0 +1,42 @@
+use std::collections::{BTreeMap, BTreeSet};
+
+use proxmox_frr::FrrConfig;
+
+use crate::common::valid::Valid;
+use crate::sdn::fabric::{section_config::node::NodeId, FabricConfig};
+
+/// Builder that helps constructing the FrrConfig.
+///
+/// The goal is to have one struct collect all the rust-based configurations and then construct the
+/// [`FrrConfig`] from it using the build method. In the future the controller configuration will
+/// be added here as well.
+#[derive(Default)]
+pub struct FrrConfigBuilder {
+ fabrics: Valid<FabricConfig>,
+}
+
+impl FrrConfigBuilder {
+ /// Add fabric configuration to the builder
+ pub fn add_fabrics(mut self, fabric: Valid<FabricConfig>) -> FrrConfigBuilder {
+ self.fabrics = fabric;
+ self
+ }
+
+ /// Build the complete [`FrrConfig`] from this builder configuration given the hostname of the
+ /// node for which we want to build the config. We also inject the common fabric-level options
+ /// into the interfaces here. (e.g. the fabric-level "hello-interval" gets added to every
+ /// interface if there isn't a more specific one.)
+ pub fn build(self, current_node: NodeId) -> Result<FrrConfig, anyhow::Error> {
+ let mut frr_config = FrrConfig {
+ router: BTreeMap::new(),
+ interfaces: BTreeMap::new(),
+ access_lists: Vec::new(),
+ routemaps: Vec::new(),
+ protocol_routemaps: BTreeSet::new(),
+ };
+
+ crate::sdn::fabric::frr::build_fabric(current_node, self.fabrics, &mut frr_config)?;
+
+ Ok(frr_config)
+ }
+}
diff --git a/proxmox-ve-config/src/sdn/mod.rs b/proxmox-ve-config/src/sdn/mod.rs
index 7a46db3..6252601 100644
--- a/proxmox-ve-config/src/sdn/mod.rs
+++ b/proxmox-ve-config/src/sdn/mod.rs
@@ -1,5 +1,7 @@
pub mod config;
pub mod fabric;
+#[cfg(feature = "frr")]
+pub mod frr;
pub mod ipam;
use std::{error::Error, fmt::Display, str::FromStr};
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-ve-rs v3 21/21] ve-config: add integrations tests
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (24 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 20/21] sdn: fabrics: implement FRR configuration generation Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-perl-rs v3 1/5] pve-rs: Add PVE::RS::SDN::Fabrics module Stefan Hanreich
` (48 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
Add integration tests for the full cycle from section-config to FRR
config file for both openfabric and ospf. It tests everything
end-to-end, from reading the configuration file to converting it into
a FabricConfig and then serializing an FRR configuration from it.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
proxmox-ve-config/Cargo.toml | 3 +
.../fabric/cfg/openfabric_default/fabrics.cfg | 18 +++
.../cfg/openfabric_dualstack/fabrics.cfg | 22 +++
.../cfg/openfabric_ipv6_only/fabrics.cfg | 18 +++
.../cfg/openfabric_loopback/fabrics.cfg | 18 +++
.../fabrics.cfg | 25 ++++
.../cfg/openfabric_multi_fabric/fabrics.cfg | 25 ++++
.../fabrics.cfg | 25 ++++
.../openfabric_verification_fail/fabrics.cfg | 12 ++
.../tests/fabric/cfg/ospf_default/fabrics.cfg | 13 ++
.../cfg/ospf_loopback_prefix_fail/fabrics.cfg | 17 +++
.../fabric/cfg/ospf_multi_fabric/fabrics.cfg | 25 ++++
.../cfg/ospf_verification_fail/fabrics.cfg | 13 ++
proxmox-ve-config/tests/fabric/helper.rs | 43 ++++++
proxmox-ve-config/tests/fabric/main.rs | 141 ++++++++++++++++++
.../fabric__openfabric_default_pve.snap | 34 +++++
.../fabric__openfabric_default_pve1.snap | 33 ++++
.../fabric__openfabric_dualstack_pve.snap | 46 ++++++
.../fabric__openfabric_ipv6_only_pve.snap | 34 +++++
.../fabric__openfabric_multi_fabric_pve1.snap | 49 ++++++
.../snapshots/fabric__ospf_default_pve.snap | 32 ++++
.../snapshots/fabric__ospf_default_pve1.snap | 28 ++++
.../fabric__ospf_multi_fabric_pve1.snap | 45 ++++++
23 files changed, 719 insertions(+)
create mode 100644 proxmox-ve-config/tests/fabric/cfg/openfabric_default/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/cfg/openfabric_dualstack/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/cfg/openfabric_ipv6_only/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/cfg/openfabric_loopback/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/cfg/openfabric_loopback_prefix_fail/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/cfg/openfabric_multi_fabric/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/cfg/openfabric_same_net_on_same_node/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/cfg/openfabric_verification_fail/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/cfg/ospf_default/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/cfg/ospf_loopback_prefix_fail/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/cfg/ospf_multi_fabric/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/cfg/ospf_verification_fail/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/helper.rs
create mode 100644 proxmox-ve-config/tests/fabric/main.rs
create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve.snap
create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve1.snap
create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_dualstack_pve.snap
create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_ipv6_only_pve.snap
create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_multi_fabric_pve1.snap
create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve.snap
create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve1.snap
create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_multi_fabric_pve1.snap
diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml
index cea541e..eca5299 100644
--- a/proxmox-ve-config/Cargo.toml
+++ b/proxmox-ve-config/Cargo.toml
@@ -31,3 +31,6 @@ proxmox-sortable-macro = "0.1.3"
[features]
frr = ["dep:proxmox-frr"]
+
+[dev-dependencies]
+insta = "1.21"
diff --git a/proxmox-ve-config/tests/fabric/cfg/openfabric_default/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/openfabric_default/fabrics.cfg
new file mode 100644
index 0000000..3df8450
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/openfabric_default/fabrics.cfg
@@ -0,0 +1,18 @@
+openfabric_fabric: uwu
+ hello_interval 4
+ ip_prefix 192.168.2.0/24
+
+openfabric_node: uwu_pve
+ interfaces name=ens20,hello_multiplier=50
+ interfaces name=ens19
+ ip 192.168.2.8
+
+openfabric_node: uwu_pve1
+ interfaces name=ens19
+ interfaces name=ens20
+ ip 192.168.2.9
+
+openfabric_node: uwu_pve2
+ interfaces name=ens19
+ interfaces name=ens20
+ ip 192.168.2.10
diff --git a/proxmox-ve-config/tests/fabric/cfg/openfabric_dualstack/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/openfabric_dualstack/fabrics.cfg
new file mode 100644
index 0000000..fe1e986
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/openfabric_dualstack/fabrics.cfg
@@ -0,0 +1,22 @@
+openfabric_fabric: uwu
+ hello_interval 4
+ ip_prefix 192.168.2.0/24
+ ip6_prefix 2001:db8::0/64
+
+openfabric_node: uwu_pve
+ interfaces name=ens20,hello_multiplier=50
+ interfaces name=ens19
+ ip 192.168.2.8
+ ip6 2001:db8::1
+
+openfabric_node: uwu_pve1
+ interfaces name=ens19
+ interfaces name=ens20
+ ip 192.168.2.9
+ ip6 2001:db8::2
+
+openfabric_node: uwu_pve2
+ interfaces name=ens19
+ interfaces name=ens20
+ ip 192.168.2.10
+ ip6 2001:db8::3
diff --git a/proxmox-ve-config/tests/fabric/cfg/openfabric_ipv6_only/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/openfabric_ipv6_only/fabrics.cfg
new file mode 100644
index 0000000..286a5a3
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/openfabric_ipv6_only/fabrics.cfg
@@ -0,0 +1,18 @@
+openfabric_fabric: uwu
+ hello_interval 4
+ ip6_prefix a:b::0/75
+
+openfabric_node: uwu_pve
+ interfaces name=ens20,hello_multiplier=50
+ interfaces name=ens19
+ ip6 a:b::a
+
+openfabric_node: uwu_pve1
+ interfaces name=ens19
+ interfaces name=ens20
+ ip6 a:b::b
+
+openfabric_node: uwu_pve2
+ interfaces name=ens19
+ interfaces name=ens20
+ ip6 a:b::c
diff --git a/proxmox-ve-config/tests/fabric/cfg/openfabric_loopback/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/openfabric_loopback/fabrics.cfg
new file mode 100644
index 0000000..ea93eb7
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/openfabric_loopback/fabrics.cfg
@@ -0,0 +1,18 @@
+openfabric_fabric: test
+ hello_interval 4
+ ip_prefix 192.168.2.0/28
+
+openfabric_node: test_pve
+ interfaces name=ens20,hello_multiplier=50
+ interfaces name=ens19
+ ip 192.168.2.8
+
+openfabric_node: test_pve1
+ interfaces name=ens19
+ interfaces name=ens20
+ ip 192.168.2.20
+
+openfabric_node: test_pve2
+ interfaces name=ens19
+ interfaces name=ens20
+ ip 192.168.2.10
diff --git a/proxmox-ve-config/tests/fabric/cfg/openfabric_loopback_prefix_fail/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/openfabric_loopback_prefix_fail/fabrics.cfg
new file mode 100644
index 0000000..46acd1d
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/openfabric_loopback_prefix_fail/fabrics.cfg
@@ -0,0 +1,25 @@
+openfabric_fabric: test
+ hello_interval 4
+ ip_prefix 192.168.2.0/28
+
+openfabric_node: test_pve
+ fabric_id test
+ interfaces name=ens20,hello_multiplier=50
+ interfaces name=ens19
+ node_id pve
+ ip 192.168.2.8
+
+openfabric_node: test_pve1
+ fabric_id test
+ interfaces name=ens19
+ interfaces name=ens20
+ node_id pve1
+ ip 192.168.2.20
+
+openfabric_node: test_pve2
+ fabric_id test
+ interfaces name=ens19
+ interfaces name=ens20
+ node_id pve2
+ ip 192.168.2.10
+
diff --git a/proxmox-ve-config/tests/fabric/cfg/openfabric_multi_fabric/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/openfabric_multi_fabric/fabrics.cfg
new file mode 100644
index 0000000..dcfdfa7
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/openfabric_multi_fabric/fabrics.cfg
@@ -0,0 +1,25 @@
+openfabric_fabric: test1
+ hello_interval 4
+ ip_prefix 192.168.2.0/24
+
+openfabric_fabric: test2
+ hello_interval 4
+ ip_prefix 192.168.1.0/24
+
+openfabric_node: test1_pve
+ interfaces name=ens20,hello_multiplier=50
+ interfaces name=ens19,
+ ip 192.168.2.8
+
+openfabric_node: test1_pve1
+ interfaces name=ens19
+ ip 192.168.2.9
+
+openfabric_node: test2_pve
+ interfaces name=ens22,hello_multiplier=50
+ interfaces name=ens21
+ ip 192.168.1.8
+
+openfabric_node: test2_pve1
+ interfaces name=ens21
+ ip 192.168.1.9
diff --git a/proxmox-ve-config/tests/fabric/cfg/openfabric_same_net_on_same_node/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/openfabric_same_net_on_same_node/fabrics.cfg
new file mode 100644
index 0000000..dcfdfa7
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/openfabric_same_net_on_same_node/fabrics.cfg
@@ -0,0 +1,25 @@
+openfabric_fabric: test1
+ hello_interval 4
+ ip_prefix 192.168.2.0/24
+
+openfabric_fabric: test2
+ hello_interval 4
+ ip_prefix 192.168.1.0/24
+
+openfabric_node: test1_pve
+ interfaces name=ens20,hello_multiplier=50
+ interfaces name=ens19,
+ ip 192.168.2.8
+
+openfabric_node: test1_pve1
+ interfaces name=ens19
+ ip 192.168.2.9
+
+openfabric_node: test2_pve
+ interfaces name=ens22,hello_multiplier=50
+ interfaces name=ens21
+ ip 192.168.1.8
+
+openfabric_node: test2_pve1
+ interfaces name=ens21
+ ip 192.168.1.9
diff --git a/proxmox-ve-config/tests/fabric/cfg/openfabric_verification_fail/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/openfabric_verification_fail/fabrics.cfg
new file mode 100644
index 0000000..5f26a9c
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/openfabric_verification_fail/fabrics.cfg
@@ -0,0 +1,12 @@
+openfabric_fabric: uwu
+ ip_prefix 192.168.2.0/24
+
+openfabric_node: uwu1_pve
+ interfaces name=ens20,hello_multiplier=50
+ interfaces name=ens19
+ ip 192.168.2.8
+
+openfabric_node: uwu_pve1
+ interfaces name=ens19
+ interfaces name=ens20
+ ip 192.168.2.9
diff --git a/proxmox-ve-config/tests/fabric/cfg/ospf_default/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/ospf_default/fabrics.cfg
new file mode 100644
index 0000000..8f2d054
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/ospf_default/fabrics.cfg
@@ -0,0 +1,13 @@
+ospf_fabric: test
+ area 0
+ ip_prefix 10.10.10.10/24
+
+ospf_node: test_pve
+ interfaces name=ens18,ip=4.4.4.4/24
+ interfaces name=ens19
+ ip 10.10.10.1
+
+ospf_node: test_pve1
+ interfaces name=ens19
+ ip 10.10.10.2
+
diff --git a/proxmox-ve-config/tests/fabric/cfg/ospf_loopback_prefix_fail/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/ospf_loopback_prefix_fail/fabrics.cfg
new file mode 100644
index 0000000..8e3b8ba
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/ospf_loopback_prefix_fail/fabrics.cfg
@@ -0,0 +1,17 @@
+ospf_fabric: test
+ ip_prefix 192.168.2.0/16
+
+ospf_node: test_pve
+ interfaces name=ens20
+ interfaces name=ens19
+ ip 192.168.2.8
+
+ospf_node: test_pve1
+ interfaces name=ens19
+ interfaces name=ens20
+ ip 192.168.3.20
+
+ospf_node: test_pve2
+ interfaces name=ens19,ip=3.3.3.2/31
+ interfaces name=ens20,ip=3.3.3.4/31
+ ip 192.169.2.10
diff --git a/proxmox-ve-config/tests/fabric/cfg/ospf_multi_fabric/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/ospf_multi_fabric/fabrics.cfg
new file mode 100644
index 0000000..36dd573
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/ospf_multi_fabric/fabrics.cfg
@@ -0,0 +1,25 @@
+ospf_fabric: test
+ area 0
+ ip_prefix 192.168.2.0/24
+
+ospf_fabric: ceph
+ area 1
+ ip_prefix 192.168.1.0/24
+
+ospf_node: test_pve
+ interfaces name=ens20,
+ interfaces name=ens19,ip=3.3.3.4/31
+ ip 192.168.2.8
+
+ospf_node: test_pve1
+ interfaces name=ens19
+ ip 192.168.2.9
+
+ospf_node: ceph_pve
+ interfaces name=ens22
+ interfaces name=ens21
+ ip 192.168.1.8
+
+ospf_node: ceph_pve1
+ interfaces name=ens21
+ ip 192.168.1.9
diff --git a/proxmox-ve-config/tests/fabric/cfg/ospf_verification_fail/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/ospf_verification_fail/fabrics.cfg
new file mode 100644
index 0000000..d1a4509
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/ospf_verification_fail/fabrics.cfg
@@ -0,0 +1,13 @@
+ospf_fabric: test
+ area 0
+ ip_prefix 10.10.10.0/24
+
+ospf_node: test_pve
+ interfaces name=dummy0
+ interfaces name=ens18
+ ip 10.10.10.1
+
+ospf_node: test1_pve1
+ interfaces name=dummy0
+ interfaces name=ens19
+ ip 10.10.10.2
diff --git a/proxmox-ve-config/tests/fabric/helper.rs b/proxmox-ve-config/tests/fabric/helper.rs
new file mode 100644
index 0000000..93404b8
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/helper.rs
@@ -0,0 +1,43 @@
+#[allow(unused_macros)]
+macro_rules! get_fabrics_config {
+ () => {{
+ // Get current function name
+ fn f() {}
+ fn type_name_of<T>(_: T) -> &'static str {
+ std::any::type_name::<T>()
+ }
+ let mut name = type_name_of(f);
+
+ // Find and cut the rest of the path
+ name = match &name[..name.len() - 3].rfind(':') {
+ Some(pos) => &name[pos + 1..name.len() - 3],
+ None => &name[..name.len() - 3],
+ };
+ let real_filename = format!("tests/fabric/cfg/{name}/fabrics.cfg");
+ &std::fs::read_to_string(real_filename).expect("cannot find config file")
+ }};
+}
+
+#[allow(unused_macros)]
+macro_rules! reference_name {
+ ($suffix:expr) => {{
+ // Get current function name
+ fn f() {}
+ fn type_name_of<T>(_: T) -> &'static str {
+ std::any::type_name::<T>()
+ }
+ let mut name = type_name_of(f);
+
+ // Find and cut the rest of the path
+ name = match &name[..name.len() - 3].rfind(':') {
+ Some(pos) => &name[pos + 1..name.len() - 3],
+ None => &name[..name.len() - 3],
+ };
+ format!("{name}_{}", $suffix)
+ }};
+}
+
+#[allow(unused_imports)]
+pub(crate) use get_fabrics_config;
+#[allow(unused_imports)]
+pub(crate) use reference_name;
diff --git a/proxmox-ve-config/tests/fabric/main.rs b/proxmox-ve-config/tests/fabric/main.rs
new file mode 100644
index 0000000..47bbbeb
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/main.rs
@@ -0,0 +1,141 @@
+#![cfg(feature = "frr")]
+use proxmox_frr::serializer::dump;
+use proxmox_ve_config::sdn::{
+ fabric::{section_config::node::NodeId, FabricConfig},
+ frr::FrrConfigBuilder,
+};
+
+mod helper;
+
+/*
+ * Use the macros `helper::get_section_config!()` to get the section config as a string. This uses
+ * the function name and checks for "/resources/cfg/{function-name}/fabrics.cfg" files.
+ * With the `helper::reference_name!("<hostname>")` macro you can get the snapshot file of the
+ * function for this specific hostname.
+ */
+
+#[test]
+fn openfabric_default() {
+ let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
+
+ let mut frr_config = FrrConfigBuilder::default()
+ .add_fabrics(config.clone())
+ .build(NodeId::from_string("pve".to_owned()).expect("invalid nodeid"))
+ .expect("error building frr config");
+
+ let mut output = dump(&frr_config).expect("error dumping stuff");
+
+ insta::assert_snapshot!(helper::reference_name!("pve"), output);
+
+ frr_config = FrrConfigBuilder::default()
+ .add_fabrics(config.clone())
+ .build(NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"))
+ .expect("error building frr config");
+
+ output = dump(&frr_config).expect("error dumping stuff");
+
+ insta::assert_snapshot!(helper::reference_name!("pve1"), output);
+}
+
+#[test]
+fn ospf_default() {
+ let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
+
+ let mut frr_config = FrrConfigBuilder::default()
+ .add_fabrics(config.clone())
+ .build(NodeId::from_string("pve".to_owned()).expect("invalid nodeid"))
+ .expect("error building frr config");
+
+ let mut output = dump(&frr_config).expect("error dumping stuff");
+
+ insta::assert_snapshot!(helper::reference_name!("pve"), output);
+
+ frr_config = FrrConfigBuilder::default()
+ .add_fabrics(config)
+ .build(NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"))
+ .expect("error building frr config");
+
+ output = dump(&frr_config).expect("error dumping stuff");
+
+ insta::assert_snapshot!(helper::reference_name!("pve1"), output);
+}
+
+#[test]
+fn openfabric_verification_fail() {
+ let result = FabricConfig::parse_section_config(helper::get_fabrics_config!());
+ assert!(result.is_err());
+}
+
+#[test]
+fn ospf_verification_fail() {
+ let result = FabricConfig::parse_section_config(helper::get_fabrics_config!());
+ assert!(result.is_err());
+}
+
+#[test]
+fn openfabric_loopback_prefix_fail() {
+ let result = FabricConfig::parse_section_config(helper::get_fabrics_config!());
+ assert!(result.is_err());
+}
+
+#[test]
+fn ospf_loopback_prefix_fail() {
+ let result = FabricConfig::parse_section_config(helper::get_fabrics_config!());
+ assert!(result.is_err());
+}
+
+#[test]
+fn openfabric_multi_fabric() {
+ let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
+
+ let frr_config = FrrConfigBuilder::default()
+ .add_fabrics(config)
+ .build(NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"))
+ .expect("error building frr config");
+
+ let output = dump(&frr_config).expect("error dumping stuff");
+
+ insta::assert_snapshot!(helper::reference_name!("pve1"), output);
+}
+
+#[test]
+fn ospf_multi_fabric() {
+ let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
+
+ let frr_config = FrrConfigBuilder::default()
+ .add_fabrics(config)
+ .build(NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"))
+ .expect("error building frr config");
+
+ let output = dump(&frr_config).expect("error dumping stuff");
+
+ insta::assert_snapshot!(helper::reference_name!("pve1"), output);
+}
+
+#[test]
+fn openfabric_dualstack() {
+ let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
+
+ let frr_config = FrrConfigBuilder::default()
+ .add_fabrics(config)
+ .build(NodeId::from_string("pve".to_owned()).expect("invalid nodeid"))
+ .expect("error building frr config");
+
+ let output = dump(&frr_config).expect("error dumping stuff");
+
+ insta::assert_snapshot!(helper::reference_name!("pve"), output);
+}
+
+#[test]
+fn openfabric_ipv6_only() {
+ let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
+
+ let frr_config = FrrConfigBuilder::default()
+ .add_fabrics(config)
+ .build(NodeId::from_string("pve".to_owned()).expect("invalid nodeid"))
+ .expect("error building frr config");
+
+ let output = dump(&frr_config).expect("error dumping stuff");
+
+ insta::assert_snapshot!(helper::reference_name!("pve"), output);
+}
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve.snap
new file mode 100644
index 0000000..98eb504
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve.snap
@@ -0,0 +1,34 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+snapshot_kind: text
+---
+router openfabric uwu
+ net 49.0001.1921.6800.2008.00
+exit
+!
+interface dummy_uwu
+ ip router openfabric uwu
+ openfabric passive
+exit
+!
+interface ens19
+ ip router openfabric uwu
+ openfabric hello-interval 4
+exit
+!
+interface ens20
+ ip router openfabric uwu
+ openfabric hello-interval 4
+ openfabric hello-multiplier 50
+exit
+!
+access-list pve_openfabric_uwu_ips permit 192.168.2.0/24
+!
+route-map pve_openfabric permit 100
+ match ip address pve_openfabric_uwu_ips
+ set src 192.168.2.8
+exit
+!
+ip protocol openfabric route-map pve_openfabric
+!
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve1.snap
new file mode 100644
index 0000000..4453ac4
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve1.snap
@@ -0,0 +1,33 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+snapshot_kind: text
+---
+router openfabric uwu
+ net 49.0001.1921.6800.2009.00
+exit
+!
+interface dummy_uwu
+ ip router openfabric uwu
+ openfabric passive
+exit
+!
+interface ens19
+ ip router openfabric uwu
+ openfabric hello-interval 4
+exit
+!
+interface ens20
+ ip router openfabric uwu
+ openfabric hello-interval 4
+exit
+!
+access-list pve_openfabric_uwu_ips permit 192.168.2.0/24
+!
+route-map pve_openfabric permit 100
+ match ip address pve_openfabric_uwu_ips
+ set src 192.168.2.9
+exit
+!
+ip protocol openfabric route-map pve_openfabric
+!
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_dualstack_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_dualstack_pve.snap
new file mode 100644
index 0000000..48ac909
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_dualstack_pve.snap
@@ -0,0 +1,46 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+snapshot_kind: text
+---
+router openfabric uwu
+ net 49.0001.1921.6800.2008.00
+exit
+!
+interface dummy_uwu
+ ipv6 router openfabric uwu
+ ip router openfabric uwu
+ openfabric passive
+exit
+!
+interface ens19
+ ipv6 router openfabric uwu
+ ip router openfabric uwu
+ openfabric hello-interval 4
+exit
+!
+interface ens20
+ ipv6 router openfabric uwu
+ ip router openfabric uwu
+ openfabric hello-interval 4
+ openfabric hello-multiplier 50
+exit
+!
+access-list pve_openfabric_uwu_ips permit 192.168.2.0/24
+!
+ipv6 access-list pve_openfabric_uwu_ip6s permit 2001:db8::/64
+!
+route-map pve_openfabric permit 100
+ match ip address pve_openfabric_uwu_ips
+ set src 192.168.2.8
+exit
+!
+route-map pve_openfabric6 permit 110
+ match ipv6 address pve_openfabric_uwu_ip6s
+ set src 2001:db8::1
+exit
+!
+ip protocol openfabric route-map pve_openfabric
+!
+ipv6 protocol openfabric route-map pve_openfabric6
+!
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_ipv6_only_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_ipv6_only_pve.snap
new file mode 100644
index 0000000..d7ab1d7
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_ipv6_only_pve.snap
@@ -0,0 +1,34 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+snapshot_kind: text
+---
+router openfabric uwu
+ net 49.0001.0000.0000.000a.00
+exit
+!
+interface dummy_uwu
+ ipv6 router openfabric uwu
+ openfabric passive
+exit
+!
+interface ens19
+ ipv6 router openfabric uwu
+ openfabric hello-interval 4
+exit
+!
+interface ens20
+ ipv6 router openfabric uwu
+ openfabric hello-interval 4
+ openfabric hello-multiplier 50
+exit
+!
+ipv6 access-list pve_openfabric_uwu_ip6s permit a:b::/75
+!
+route-map pve_openfabric6 permit 100
+ match ipv6 address pve_openfabric_uwu_ip6s
+ set src a:b::a
+exit
+!
+ipv6 protocol openfabric route-map pve_openfabric6
+!
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_multi_fabric_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_multi_fabric_pve1.snap
new file mode 100644
index 0000000..ad6c6db
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_multi_fabric_pve1.snap
@@ -0,0 +1,49 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+snapshot_kind: text
+---
+router openfabric test1
+ net 49.0001.1921.6800.2009.00
+exit
+!
+router openfabric test2
+ net 49.0001.1921.6800.2009.00
+exit
+!
+interface dummy_test1
+ ip router openfabric test1
+ openfabric passive
+exit
+!
+interface dummy_test2
+ ip router openfabric test2
+ openfabric passive
+exit
+!
+interface ens19
+ ip router openfabric test1
+ openfabric hello-interval 4
+exit
+!
+interface ens21
+ ip router openfabric test2
+ openfabric hello-interval 4
+exit
+!
+access-list pve_openfabric_test1_ips permit 192.168.2.0/24
+!
+access-list pve_openfabric_test2_ips permit 192.168.1.0/24
+!
+route-map pve_openfabric permit 100
+ match ip address pve_openfabric_test1_ips
+ set src 192.168.2.9
+exit
+!
+route-map pve_openfabric permit 110
+ match ip address pve_openfabric_test2_ips
+ set src 192.168.1.9
+exit
+!
+ip protocol openfabric route-map pve_openfabric
+!
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve.snap
new file mode 100644
index 0000000..a303f31
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve.snap
@@ -0,0 +1,32 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+snapshot_kind: text
+---
+router ospf
+ ospf router-id 10.10.10.1
+exit
+!
+interface dummy_test
+ ip ospf area 0
+ ip ospf passive
+exit
+!
+interface ens18
+ ip ospf area 0
+exit
+!
+interface ens19
+ ip ospf area 0
+ ip ospf network point-to-point
+exit
+!
+access-list pve_ospf_test_ips permit 10.10.10.10/24
+!
+route-map pve_ospf permit 100
+ match ip address pve_ospf_test_ips
+ set src 10.10.10.1
+exit
+!
+ip protocol ospf route-map pve_ospf
+!
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve1.snap
new file mode 100644
index 0000000..46c30b2
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve1.snap
@@ -0,0 +1,28 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+snapshot_kind: text
+---
+router ospf
+ ospf router-id 10.10.10.2
+exit
+!
+interface dummy_test
+ ip ospf area 0
+ ip ospf passive
+exit
+!
+interface ens19
+ ip ospf area 0
+ ip ospf network point-to-point
+exit
+!
+access-list pve_ospf_test_ips permit 10.10.10.10/24
+!
+route-map pve_ospf permit 100
+ match ip address pve_ospf_test_ips
+ set src 10.10.10.2
+exit
+!
+ip protocol ospf route-map pve_ospf
+!
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_multi_fabric_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_multi_fabric_pve1.snap
new file mode 100644
index 0000000..1d2a7c3
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_multi_fabric_pve1.snap
@@ -0,0 +1,45 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+snapshot_kind: text
+---
+router ospf
+ ospf router-id 192.168.1.9
+exit
+!
+interface dummy_ceph
+ ip ospf area 1
+ ip ospf passive
+exit
+!
+interface dummy_test
+ ip ospf area 0
+ ip ospf passive
+exit
+!
+interface ens19
+ ip ospf area 0
+ ip ospf network point-to-point
+exit
+!
+interface ens21
+ ip ospf area 1
+ ip ospf network point-to-point
+exit
+!
+access-list pve_ospf_ceph_ips permit 192.168.1.0/24
+!
+access-list pve_ospf_test_ips permit 192.168.2.0/24
+!
+route-map pve_ospf permit 100
+ match ip address pve_ospf_ceph_ips
+ set src 192.168.1.9
+exit
+!
+route-map pve_ospf permit 110
+ match ip address pve_ospf_test_ips
+ set src 192.168.2.9
+exit
+!
+ip protocol ospf route-map pve_ospf
+!
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-perl-rs v3 1/5] pve-rs: Add PVE::RS::SDN::Fabrics module
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (25 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 21/21] ve-config: add integrations tests Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-perl-rs v3 2/5] pve-rs: sdn: fabrics: add api methods Stefan Hanreich
` (47 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
This module exposes the functionality provided proxmox-ve-config for
the SDN fabrics to perl. We add initial support for reading and
writing the section config stored in /etc/pve/sdn/fabrics.cfg as well
as the running configuration, stored in /etc/pve/sdn/.running-config.
It also provides a helper method for calculating the digest of the
configuration.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pve-rs/Cargo.toml | 4 +-
pve-rs/Makefile | 1 +
pve-rs/debian/control | 2 +
pve-rs/src/bindings/mod.rs | 3 +
pve-rs/src/bindings/sdn/fabrics.rs | 95 ++++++++++++++++++++++++++++++
pve-rs/src/bindings/sdn/mod.rs | 1 +
6 files changed, 105 insertions(+), 1 deletion(-)
create mode 100644 pve-rs/src/bindings/sdn/fabrics.rs
create mode 100644 pve-rs/src/bindings/sdn/mod.rs
diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml
index 82ebce4..fbeff72 100644
--- a/pve-rs/Cargo.toml
+++ b/pve-rs/Cargo.toml
@@ -33,15 +33,17 @@ perlmod = { version = "0.13.5", features = ["exporter"] }
proxmox-apt = { version = "0.11.5", features = ["cache"] }
proxmox-apt-api-types = "1.0"
proxmox-config-digest = "0.1"
+proxmox-frr = { version = "0.1" }
proxmox-http = { version = "0.9", features = ["client-sync", "client-trait"] }
proxmox-http-error = "0.1.0"
proxmox-log = "0.2"
proxmox-notify = { version = "0.5.4", features = ["pve-context"] }
proxmox-openid = "0.10.4"
proxmox-resource-scheduling = "0.3.0"
+proxmox-section-config = "3"
proxmox-shared-cache = "0.1.0"
proxmox-subscription = "0.5"
proxmox-sys = "0.6"
proxmox-tfa = { version = "5", features = ["api"] }
proxmox-time = "2"
-proxmox-ve-config = { version = "0.2.1" }
+proxmox-ve-config = { version = "0.2.2", features = [ "frr" ] }
diff --git a/pve-rs/Makefile b/pve-rs/Makefile
index 4a5a277..56e78d4 100644
--- a/pve-rs/Makefile
+++ b/pve-rs/Makefile
@@ -30,6 +30,7 @@ PERLMOD_PACKAGES := \
PVE::RS::Firewall::SDN \
PVE::RS::OpenId \
PVE::RS::ResourceScheduling::Static \
+ PVE::RS::SDN::Fabrics \
PVE::RS::TFA
PERLMOD_PACKAGE_FILES := $(addsuffix .pm,$(subst ::,/,$(PERLMOD_PACKAGES)))
diff --git a/pve-rs/debian/control b/pve-rs/debian/control
index 1eaca52..9d772d8 100644
--- a/pve-rs/debian/control
+++ b/pve-rs/debian/control
@@ -18,6 +18,7 @@ Build-Depends: cargo:native <!nocheck>,
librust-proxmox-apt-0.11+default-dev (>= 0.11.5-~~),
librust-proxmox-apt-api-types-1+default-dev,
librust-proxmox-config-digest-0.1+default-dev,
+ librust-proxmox-frr-0.1+default-dev,
librust-proxmox-http-0.9+client-sync-dev,
librust-proxmox-http-0.9+client-trait-dev,
librust-proxmox-http-0.9+default-dev,
@@ -34,6 +35,7 @@ Build-Depends: cargo:native <!nocheck>,
librust-proxmox-tfa-5+default-dev,
librust-proxmox-time-2+default-dev,
librust-proxmox-ve-config-dev (>= 0.2.1-~~),
+ librust-proxmox-ve-config+frr-dev (>= 0.2.2-~~),
librust-serde-1+default-dev,
librust-serde-bytes-0.11+default-dev,
librust-serde-json-1+default-dev,
diff --git a/pve-rs/src/bindings/mod.rs b/pve-rs/src/bindings/mod.rs
index a6a7c6f..79a9917 100644
--- a/pve-rs/src/bindings/mod.rs
+++ b/pve-rs/src/bindings/mod.rs
@@ -8,6 +8,9 @@ pub use resource_scheduling_static::pve_rs_resource_scheduling_static;
mod tfa;
pub use tfa::pve_rs_tfa;
+mod sdn;
+pub use sdn::fabrics::pve_rs_sdn_fabrics;
+
#[allow(unused_imports)]
pub use crate::common::bindings::*;
diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
new file mode 100644
index 0000000..fac5602
--- /dev/null
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -0,0 +1,95 @@
+#[perlmod::package(name = "PVE::RS::SDN::Fabrics", lib = "pve_rs")]
+pub mod pve_rs_sdn_fabrics {
+ //! The `PVE::RS::SDN::Fabrics` package.
+ //!
+ //! This provides the configuration for the SDN fabrics, as well as helper methods for reading
+ //! / writing the configuration, as well as for generating ifupdown2 and FRR configuration.
+
+ use std::collections::BTreeMap;
+ use std::ops::Deref;
+ use std::sync::Mutex;
+
+ use anyhow::Error;
+ use openssl::hash::{hash, MessageDigest};
+ use serde::{Deserialize, Serialize};
+
+ use perlmod::Value;
+ use proxmox_section_config::typed::SectionConfigData;
+ use proxmox_ve_config::common::valid::Validatable;
+
+ use proxmox_ve_config::sdn::fabric::{section_config::Section, FabricConfig};
+
+ /// A SDN Fabric config instance.
+ #[derive(Serialize, Deserialize)]
+ pub struct PerlFabricConfig {
+ /// The fabric config instance
+ pub fabric_config: Mutex<FabricConfig>,
+ }
+
+ perlmod::declare_magic!(Box<PerlFabricConfig> : &PerlFabricConfig as "PVE::RS::SDN::Fabrics::Config");
+
+ /// Parse the raw configuration from `/etc/pve/sdn/fabrics.cfg`.
+ #[export]
+ fn config(#[raw] class: Value, raw_config: &[u8]) -> Result<perlmod::Value, Error> {
+ let raw_config = std::str::from_utf8(raw_config)?;
+ let config = FabricConfig::parse_section_config(raw_config)?;
+
+ Ok(
+ perlmod::instantiate_magic!(&class, MAGIC => Box::new(PerlFabricConfig {
+ fabric_config: Mutex::new(config.into_inner()),
+ })),
+ )
+ }
+
+ /// Parse the configuration from `/etc/pve/sdn/.running_config`.
+ #[export]
+ fn running_config(
+ #[raw] class: Value,
+ fabrics: BTreeMap<String, Section>,
+ ) -> Result<perlmod::Value, Error> {
+ let fabrics = SectionConfigData::from_iter(fabrics);
+ let config = FabricConfig::from_section_config(fabrics)?;
+
+ Ok(
+ perlmod::instantiate_magic!(&class, MAGIC => Box::new(PerlFabricConfig {
+ fabric_config: Mutex::new(config.into_inner()),
+ })),
+ )
+ }
+
+ /// Class method: Convert the configuration into the section config sections.
+ ///
+ /// Used for writing the running configuration.
+ #[export]
+ fn to_sections(
+ #[try_from_ref] this: &PerlFabricConfig,
+ ) -> Result<BTreeMap<String, Section>, Error> {
+ let config = this
+ .fabric_config
+ .lock()
+ .unwrap()
+ .clone()
+ .into_valid()?
+ .into_section_config();
+
+ Ok(BTreeMap::from_iter(config.clone()))
+ }
+
+ /// Class method: Convert the configuration into the section config string.
+ ///
+ /// Used for writing `/etc/pve/sdn/fabrics.cfg`
+ #[export]
+ fn to_raw(#[try_from_ref] this: &PerlFabricConfig) -> Result<String, Error> {
+ this.fabric_config.lock().unwrap().write_section_config()
+ }
+
+ /// Class method: Generate a digest for the whole configuration
+ #[export]
+ fn digest(#[try_from_ref] this: &PerlFabricConfig) -> Result<String, Error> {
+ let config = this.fabric_config.lock().unwrap();
+ let data = serde_json::to_vec(config.deref())?;
+ let hash = hash(MessageDigest::sha256(), &data)?;
+
+ Ok(hex::encode(hash))
+ }
+}
diff --git a/pve-rs/src/bindings/sdn/mod.rs b/pve-rs/src/bindings/sdn/mod.rs
new file mode 100644
index 0000000..0ec7009
--- /dev/null
+++ b/pve-rs/src/bindings/sdn/mod.rs
@@ -0,0 +1 @@
+pub(crate) mod fabrics;
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-perl-rs v3 2/5] pve-rs: sdn: fabrics: add api methods
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (26 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-perl-rs v3 1/5] pve-rs: Add PVE::RS::SDN::Fabrics module Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-perl-rs v3 3/5] pve-rs: sdn: fabrics: add frr config generation Stefan Hanreich
` (46 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
The FabricConfig from proxmox-ve-config implements CRUD functionality
for Fabrics and Nodes stored in the section config. We expose them via
perlmod, so they can be used in the API endpoints defined in perl.
they map 1:1 to the respective API endpoints.
They are simply calling the respective implementation of FabricConfig,
and convert from / to the API representations of the Fabrics / Nodes
returned by FabricConfig.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pve-rs/src/bindings/sdn/fabrics.rs | 212 ++++++++++++++++++++++++++++-
1 file changed, 211 insertions(+), 1 deletion(-)
diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index fac5602..2efa1c6 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -17,7 +17,20 @@ pub mod pve_rs_sdn_fabrics {
use proxmox_section_config::typed::SectionConfigData;
use proxmox_ve_config::common::valid::Validatable;
- use proxmox_ve_config::sdn::fabric::{section_config::Section, FabricConfig};
+ use proxmox_ve_config::sdn::fabric::{
+ section_config::{
+ fabric::{
+ api::{Fabric, FabricUpdater},
+ FabricId,
+ },
+ node::{
+ api::{Node, NodeUpdater},
+ Node as ConfigNode, NodeId,
+ },
+ Section,
+ },
+ FabricConfig, FabricEntry,
+ };
/// A SDN Fabric config instance.
#[derive(Serialize, Deserialize)]
@@ -57,6 +70,203 @@ pub mod pve_rs_sdn_fabrics {
)
}
+ /// Class method: Returns all fabrics and nodes from the configuration.
+ #[export]
+ fn list_all(
+ #[try_from_ref] this: &PerlFabricConfig,
+ ) -> (BTreeMap<String, Fabric>, BTreeMap<String, Node>) {
+ let config = this.fabric_config.lock().unwrap();
+
+ let mut fabrics = BTreeMap::new();
+ let mut nodes = BTreeMap::new();
+
+ for entry in config.values() {
+ fabrics.insert(entry.fabric().id().to_string(), entry.fabric().clone());
+
+ nodes.extend(
+ entry
+ .nodes()
+ .map(|(_node_id, node)| (node.id().to_string(), node.clone().into())),
+ );
+ }
+
+ (fabrics, nodes)
+ }
+
+ /// Class method: Returns all fabrics from the configuration.
+ #[export]
+ fn list_fabrics(#[try_from_ref] this: &PerlFabricConfig) -> BTreeMap<String, Fabric> {
+ this.fabric_config
+ .lock()
+ .unwrap()
+ .iter()
+ .map(|(id, entry)| (id.to_string(), entry.fabric().clone()))
+ .collect()
+ }
+
+ /// Class method: Returns all fabrics configured on a specific node in the cluster.
+ #[export]
+ fn list_fabrics_by_node(
+ #[try_from_ref] this: &PerlFabricConfig,
+ node_id: NodeId,
+ ) -> BTreeMap<String, Fabric> {
+ this.fabric_config
+ .lock()
+ .unwrap()
+ .iter()
+ .filter(|(_id, entry)| entry.get_node(&node_id).is_ok())
+ .map(|(id, entry)| (id.to_string(), entry.fabric().clone()))
+ .collect()
+ }
+
+ /// Class method: Adds a new Fabric to the configuration.
+ #[export]
+ fn add_fabric(#[try_from_ref] this: &PerlFabricConfig, fabric: Fabric) -> Result<(), Error> {
+ this.fabric_config
+ .lock()
+ .unwrap()
+ .add_fabric(fabric)
+ .map_err(anyhow::Error::msg)
+ }
+
+ /// Class method: Read a Fabric from the configuration.
+ #[export]
+ fn get_fabric(#[try_from_ref] this: &PerlFabricConfig, id: FabricId) -> Result<Fabric, Error> {
+ this.fabric_config
+ .lock()
+ .unwrap()
+ .get_fabric(&id)
+ .map(|entry| entry.fabric().clone())
+ .map_err(anyhow::Error::msg)
+ }
+
+ /// Class method: Update a fabric in the configuration.
+ #[export]
+ fn update_fabric(
+ #[try_from_ref] this: &PerlFabricConfig,
+ id: FabricId,
+ updater: FabricUpdater,
+ ) -> Result<(), Error> {
+ this.fabric_config
+ .lock()
+ .unwrap()
+ .update_fabric(&id, updater)
+ .map_err(anyhow::Error::msg)
+ }
+
+ /// Class method: Delete a fabric from the configuration.
+ #[export]
+ fn delete_fabric(
+ #[try_from_ref] this: &PerlFabricConfig,
+ id: FabricId,
+ ) -> Result<FabricEntry, Error> {
+ this.fabric_config
+ .lock()
+ .unwrap()
+ .delete_fabric(&id)
+ .map_err(anyhow::Error::msg)
+ }
+
+ /// Class method: List all nodes in the configuraiton.
+ #[export]
+ fn list_nodes(
+ #[try_from_ref] this: &PerlFabricConfig,
+ ) -> Result<BTreeMap<String, Node>, Error> {
+ Ok(this
+ .fabric_config
+ .lock()
+ .unwrap()
+ .values()
+ .flat_map(|entry| {
+ entry
+ .nodes()
+ .map(|(id, node)| (id.to_string(), node.clone().into()))
+ })
+ .collect())
+ }
+
+ /// Class method: List all nodes for a specific fabric.
+ #[export]
+ fn list_nodes_fabric(
+ #[try_from_ref] this: &PerlFabricConfig,
+ fabric_id: FabricId,
+ ) -> Result<BTreeMap<String, Node>, Error> {
+ Ok(this
+ .fabric_config
+ .lock()
+ .unwrap()
+ .get_fabric(&fabric_id)
+ .map_err(anyhow::Error::msg)?
+ .nodes()
+ .map(|(id, node)| (id.to_string(), node.clone().into()))
+ .collect())
+ }
+
+ /// Class method: Get a node from a fabric.
+ #[export]
+ fn get_node(
+ #[try_from_ref] this: &PerlFabricConfig,
+ fabric_id: FabricId,
+ node_id: NodeId,
+ ) -> Result<Node, Error> {
+ this.fabric_config
+ .lock()
+ .unwrap()
+ .get_fabric(&fabric_id)
+ .map_err(anyhow::Error::msg)?
+ .get_node(&node_id)
+ .map(|node| node.clone().into())
+ .map_err(anyhow::Error::msg)
+ }
+
+ /// Class method: Add a node to a fabric.
+ #[export]
+ fn add_node(#[try_from_ref] this: &PerlFabricConfig, node: Node) -> Result<(), Error> {
+ let node = ConfigNode::from(node);
+
+ this.fabric_config
+ .lock()
+ .unwrap()
+ .get_fabric_mut(node.id().fabric_id())
+ .map_err(anyhow::Error::msg)?
+ .add_node(node)
+ .map_err(anyhow::Error::msg)
+ }
+
+ /// Class method: Update a node in a fabric.
+ #[export]
+ fn update_node(
+ #[try_from_ref] this: &PerlFabricConfig,
+ fabric_id: FabricId,
+ node_id: NodeId,
+ updater: NodeUpdater,
+ ) -> Result<(), Error> {
+ this.fabric_config
+ .lock()
+ .unwrap()
+ .get_fabric_mut(&fabric_id)
+ .map_err(anyhow::Error::msg)?
+ .update_node(&node_id, updater)
+ .map_err(anyhow::Error::msg)
+ }
+
+ /// Class method: Delete a node in a fabric.
+ #[export]
+ fn delete_node(
+ #[try_from_ref] this: &PerlFabricConfig,
+ fabric_id: FabricId,
+ node_id: NodeId,
+ ) -> Result<Node, Error> {
+ this.fabric_config
+ .lock()
+ .unwrap()
+ .get_fabric_mut(&fabric_id)
+ .map_err(anyhow::Error::msg)?
+ .delete_node(&node_id)
+ .map(Node::from)
+ .map_err(anyhow::Error::msg)
+ }
+
/// Class method: Convert the configuration into the section config sections.
///
/// Used for writing the running configuration.
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-perl-rs v3 3/5] pve-rs: sdn: fabrics: add frr config generation
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (27 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-perl-rs v3 2/5] pve-rs: sdn: fabrics: add api methods Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-perl-rs v3 4/5] pve-rs: sdn: fabrics: add helper to generate ifupdown2 configuration Stefan Hanreich
` (45 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
We use proxmox-ve-config to generate a FRR config and serialize it
with the proxmox-frr crate in order to return it to perl in its
internally used format (an array of strings). The Perl SDN module in
turn merges it with the FRR configuration generated by Perl modules
and persists it to the FRR configuration file.
We also provide a method that returns a list of daemons, that are
required to be enabled for the current FRR configuration. This is used
by Perl to write the daemons configuration file of FRR.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pve-rs/src/bindings/sdn/fabrics.rs | 49 +++++++++++++++++++++++++++++-
1 file changed, 48 insertions(+), 1 deletion(-)
diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index 2efa1c6..a7a740f 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -5,7 +5,7 @@ pub mod pve_rs_sdn_fabrics {
//! This provides the configuration for the SDN fabrics, as well as helper methods for reading
//! / writing the configuration, as well as for generating ifupdown2 and FRR configuration.
- use std::collections::BTreeMap;
+ use std::collections::{BTreeMap, HashSet};
use std::ops::Deref;
use std::sync::Mutex;
@@ -14,6 +14,7 @@ pub mod pve_rs_sdn_fabrics {
use serde::{Deserialize, Serialize};
use perlmod::Value;
+ use proxmox_frr::serializer::to_raw_config;
use proxmox_section_config::typed::SectionConfigData;
use proxmox_ve_config::common::valid::Validatable;
@@ -31,6 +32,7 @@ pub mod pve_rs_sdn_fabrics {
},
FabricConfig, FabricEntry,
};
+ use proxmox_ve_config::sdn::frr::FrrConfigBuilder;
/// A SDN Fabric config instance.
#[derive(Serialize, Deserialize)]
@@ -302,4 +304,49 @@ pub mod pve_rs_sdn_fabrics {
Ok(hex::encode(hash))
}
+
+ /// Class method: Return all FRR daemons that need to be enabled for this fabric configuration
+ /// instance.
+ #[export]
+ pub fn enabled_daemons(
+ #[try_from_ref] this: &PerlFabricConfig,
+ node_id: NodeId,
+ ) -> Vec<String> {
+ let config = this.fabric_config.lock().unwrap();
+
+ let node_fabrics = config
+ .values()
+ .filter(|fabric| fabric.get_node(&node_id).is_ok());
+
+ let mut daemons = HashSet::new();
+
+ for fabric in node_fabrics {
+ match fabric {
+ FabricEntry::Ospf(_) => {
+ daemons.insert("ospfd");
+ }
+ FabricEntry::Openfabric(_) => {
+ daemons.insert("fabricd");
+ }
+ };
+ }
+
+ daemons.into_iter().map(String::from).collect()
+ }
+
+ /// Class method: Return the FRR configuration for this config instance, as an array of
+ /// strings, where each line represents a line in the FRR configuration.
+ #[export]
+ pub fn get_frr_raw_config(
+ #[try_from_ref] this: &PerlFabricConfig,
+ node_id: NodeId,
+ ) -> Result<Vec<String>, Error> {
+ let config = this.fabric_config.lock().unwrap();
+
+ let frr_config = FrrConfigBuilder::default()
+ .add_fabrics(config.clone().into_valid()?)
+ .build(node_id)?;
+
+ to_raw_config(&frr_config)
+ }
}
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-perl-rs v3 4/5] pve-rs: sdn: fabrics: add helper to generate ifupdown2 configuration
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (28 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-perl-rs v3 3/5] pve-rs: sdn: fabrics: add frr config generation Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-perl-rs v3 5/5] pve-rs: sdn: fabrics: add helper for network API endpoint Stefan Hanreich
` (44 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
SDN fabrics can be used to configure IP addresses on interfaces
directly, so we need to generate the respective ifupdown2
configuration from the fabrics configuration. We also set some
additional properties that are required for interfaces that are part
of a fabric (IP forwarding). We use dummy interfaces, instead of
loopback interfaces, for configuring the router IP of the node, so for
each fabric we generate a dummy interface that carries the IP.
Currently this is a simple implementation that builds a String from
the SDN fabrics configuration, but in the future we intend to create a
full-fledged crate for reading / writing ifupdown2 configuration
files.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pve-rs/Cargo.toml | 1 +
pve-rs/src/bindings/sdn/fabrics.rs | 104 +++++++++++++++++++++++++++++
2 files changed, 105 insertions(+)
diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml
index fbeff72..24184fd 100644
--- a/pve-rs/Cargo.toml
+++ b/pve-rs/Cargo.toml
@@ -37,6 +37,7 @@ proxmox-frr = { version = "0.1" }
proxmox-http = { version = "0.9", features = ["client-sync", "client-trait"] }
proxmox-http-error = "0.1.0"
proxmox-log = "0.2"
+proxmox-network-types = "0.1"
proxmox-notify = { version = "0.5.4", features = ["pve-context"] }
proxmox-openid = "0.10.4"
proxmox-resource-scheduling = "0.3.0"
diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index a7a740f..099c1a7 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -6,6 +6,8 @@ pub mod pve_rs_sdn_fabrics {
//! / writing the configuration, as well as for generating ifupdown2 and FRR configuration.
use std::collections::{BTreeMap, HashSet};
+ use std::fmt::Write;
+ use std::net::IpAddr;
use std::ops::Deref;
use std::sync::Mutex;
@@ -15,6 +17,7 @@ pub mod pve_rs_sdn_fabrics {
use perlmod::Value;
use proxmox_frr::serializer::to_raw_config;
+ use proxmox_network_types::ip_address::Cidr;
use proxmox_section_config::typed::SectionConfigData;
use proxmox_ve_config::common::valid::Validatable;
@@ -349,4 +352,105 @@ pub mod pve_rs_sdn_fabrics {
to_raw_config(&frr_config)
}
+
+ /// Helper method to generate the default /e/n/i config for a given CIDR.
+ fn render_interface(name: &str, cidr: Cidr, is_dummy: bool) -> Result<String, Error> {
+ let mut interface = String::new();
+
+ writeln!(interface)?;
+ writeln!(interface, "auto {name}")?;
+ match cidr {
+ Cidr::Ipv4(_) => writeln!(interface, "iface {name} inet static")?,
+ Cidr::Ipv6(_) => writeln!(interface, "iface {name} inet6 static")?,
+ }
+ writeln!(interface, "\taddress {cidr}")?;
+ if is_dummy {
+ writeln!(interface, "\tlink-type dummy")?;
+ }
+ writeln!(interface, "\tip-forward 1")?;
+
+ Ok(interface)
+ }
+
+ /// Class method: Generate the ifupdown2 configuration for a given node.
+ #[export]
+ fn get_interfaces_etc_network_config(
+ #[try_from_ref] this: &PerlFabricConfig,
+ node_id: NodeId,
+ ) -> Result<String, Error> {
+ let config = this.fabric_config.lock().unwrap();
+ let mut interfaces = String::new();
+
+ let node_fabrics = config.values().filter_map(|entry| {
+ entry
+ .get_node(&node_id)
+ .map(|node| (entry.fabric(), node))
+ .ok()
+ });
+
+ for (fabric, node) in node_fabrics {
+ // dummy interface
+ if let Some(ip) = node.ip() {
+ let interface = render_interface(
+ &format!("dummy_{}", fabric.id()),
+ Cidr::new_v4(ip, 32)?,
+ true,
+ )?;
+ write!(interfaces, "{interface}")?;
+ }
+ if let Some(ip6) = node.ip6() {
+ let interface = render_interface(
+ &format!("dummy_{}", fabric.id()),
+ Cidr::new_v6(ip6, 128)?,
+ true,
+ )?;
+ write!(interfaces, "{interface}")?;
+ }
+ match node {
+ ConfigNode::Openfabric(node_section) => {
+ for interface in node_section.properties().interfaces() {
+ if let Some(ip) = interface.ip() {
+ let interface =
+ render_interface(interface.name(), Cidr::from(ip), false)?;
+ write!(interfaces, "{interface}")?;
+ }
+ if let Some(ip) = interface.ip6() {
+ let interface =
+ render_interface(interface.name(), Cidr::from(ip), false)?;
+ write!(interfaces, "{interface}")?;
+ }
+
+ // If not ip is configured, add auto and empty iface to bring interface up
+ if let (None, None) = (interface.ip(), interface.ip6()) {
+ writeln!(interfaces)?;
+ writeln!(interfaces, "auto {}", interface.name())?;
+ writeln!(interfaces, "iface {}", interface.name())?;
+ writeln!(interfaces, "\tip-forward 1")?;
+ }
+ }
+ }
+ ConfigNode::Ospf(node_section) => {
+ for interface in node_section.properties().interfaces() {
+ if let Some(ip) = interface.ip() {
+ let interface =
+ render_interface(interface.name(), Cidr::from(ip), false)?;
+ write!(interfaces, "{interface}")?;
+ } else {
+ let interface = render_interface(
+ interface.name(),
+ Cidr::from(IpAddr::from(
+ node.ip()
+ .ok_or(anyhow::anyhow!("there has to be a ipv4 address"))?,
+ )),
+ false,
+ )?;
+ write!(interfaces, "{interface}")?;
+ }
+ }
+ }
+ }
+ }
+
+ Ok(interfaces)
+ }
}
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-perl-rs v3 5/5] pve-rs: sdn: fabrics: add helper for network API endpoint
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (29 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-perl-rs v3 4/5] pve-rs: sdn: fabrics: add helper to generate ifupdown2 configuration Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH pve-cluster v3 1/1] cfs: add fabrics.cfg to observed files Stefan Hanreich
` (43 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
In PVE we use the GET /nodes/{node}/network API endpoint to return all
currently configured network interfaces on a specific node. In order
to be able to use SDN fabrics in Ceph and the migration settings, we
add a helper method that returns all fabrics formatted in the same
format as the pre-existing PVE API call. This enables us to return the
SDN fabrics in the endpoint so users can select the fabrics from the
UI, integrating the fabrics with the existing UI components.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pve-rs/src/bindings/sdn/fabrics.rs | 54 ++++++++++++++++++++++++++++--
1 file changed, 52 insertions(+), 2 deletions(-)
diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index 099c1a7..f5abb1b 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -17,7 +17,7 @@ pub mod pve_rs_sdn_fabrics {
use perlmod::Value;
use proxmox_frr::serializer::to_raw_config;
- use proxmox_network_types::ip_address::Cidr;
+ use proxmox_network_types::ip_address::{Cidr, Ipv4Cidr, Ipv6Cidr};
use proxmox_section_config::typed::SectionConfigData;
use proxmox_ve_config::common::valid::Validatable;
@@ -25,7 +25,7 @@ pub mod pve_rs_sdn_fabrics {
section_config::{
fabric::{
api::{Fabric, FabricUpdater},
- FabricId,
+ Fabric as ConfigFabric, FabricId,
},
node::{
api::{Node, NodeUpdater},
@@ -46,6 +46,34 @@ pub mod pve_rs_sdn_fabrics {
perlmod::declare_magic!(Box<PerlFabricConfig> : &PerlFabricConfig as "PVE::RS::SDN::Fabrics::Config");
+ /// Represents a interface as returned by the `GET /nodes/{node}/network` endpoint in PVE.
+ ///
+ /// This is used for returning fabrics in the endpoint, so they can be used from various places
+ /// in the PVE UI (e.g. migration network settings).
+ #[derive(Serialize, Deserialize)]
+ struct PveInterface {
+ iface: String,
+ #[serde(rename = "type")]
+ ty: String,
+ active: bool,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ cidr: Option<Ipv4Cidr>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ cidr6: Option<Ipv6Cidr>,
+ }
+
+ impl From<ConfigFabric> for PveInterface {
+ fn from(fabric: ConfigFabric) -> Self {
+ Self {
+ iface: fabric.id().to_string(),
+ ty: "fabric".to_string(),
+ active: true,
+ cidr: fabric.ip_prefix(),
+ cidr6: fabric.ip6_prefix(),
+ }
+ }
+ }
+
/// Parse the raw configuration from `/etc/pve/sdn/fabrics.cfg`.
#[export]
fn config(#[raw] class: Value, raw_config: &[u8]) -> Result<perlmod::Value, Error> {
@@ -308,6 +336,28 @@ pub mod pve_rs_sdn_fabrics {
Ok(hex::encode(hash))
}
+ /// Class method: Return all interfaces of a node, that are part of a fabric.
+ #[export]
+ fn get_interfaces_for_node(
+ #[try_from_ref] this: &PerlFabricConfig,
+ node_id: NodeId,
+ ) -> BTreeMap<String, PveInterface> {
+ let config = this.fabric_config.lock().unwrap();
+
+ let mut ifaces = BTreeMap::new();
+
+ for entry in config.values() {
+ if entry.get_node(&node_id).is_ok() {
+ ifaces.insert(
+ entry.fabric().id().to_string(),
+ entry.fabric().clone().into(),
+ );
+ }
+ }
+
+ ifaces
+ }
+
/// Class method: Return all FRR daemons that need to be enabled for this fabric configuration
/// instance.
#[export]
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-cluster v3 1/1] cfs: add fabrics.cfg to observed files
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (30 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-perl-rs v3 5/5] pve-rs: sdn: fabrics: add helper for network API endpoint Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH pve-access-control v3 1/1] permissions: add ACL paths for SDN fabrics Stefan Hanreich
` (42 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
In a previous commit we already added the openfabric / ospf
configuration files, but the configuration format changed since then,
so we replace them with the single configuration file used by the
fabrics now.
Also add a postinst script that removes the leftover folder from that
change.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
debian/pve-cluster.postinst | 24 ++++++++++++++++++++++++
src/PVE/Cluster.pm | 3 +--
src/pmxcfs/status.c | 3 +--
3 files changed, 26 insertions(+), 4 deletions(-)
create mode 100644 debian/pve-cluster.postinst
diff --git a/debian/pve-cluster.postinst b/debian/pve-cluster.postinst
new file mode 100644
index 0000000..5ca091d
--- /dev/null
+++ b/debian/pve-cluster.postinst
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+set -e
+
+remove_fabrics_directory() {
+ LEGACY_FABRICS_DIRECTORY="/etc/pve/sdn/fabrics/"
+
+ if test -d "$LEGACY_FABRICS_DIRECTORY"; then
+ echo "Removing legacy sdn fabrics directory ..."
+ rm -d $LEGACY_FABRICS_DIRECTORY || echo "Failed to remove legacy sdn folder ${LEGACY_FABRICS_DIRECTORY}!"
+ fi
+}
+
+case "$1" in
+ configure)
+ # TODO: remove with PVE 10+
+ if dpkg --compare-versions "$2" 'lt' '9.0.0'; then
+ remove_fabrics_directory
+ fi
+ ;;
+esac
+
+exit 0
+
diff --git a/src/PVE/Cluster.pm b/src/PVE/Cluster.pm
index 0c78e65..d400620 100644
--- a/src/PVE/Cluster.pm
+++ b/src/PVE/Cluster.pm
@@ -81,8 +81,7 @@ my $observed = {
'sdn/pve-ipam-state.json' => 1,
'sdn/mac-cache.json' => 1,
'sdn/dns.cfg' => 1,
- 'sdn/fabrics/openfabric.cfg' => 1,
- 'sdn/fabrics/ospf.cfg' => 1,
+ 'sdn/fabrics.cfg' => 1,
'sdn/.running-config' => 1,
'virtual-guest/cpu-models.conf' => 1,
'virtual-guest/profiles.cfg' => 1,
diff --git a/src/pmxcfs/status.c b/src/pmxcfs/status.c
index cda3921..2a2f673 100644
--- a/src/pmxcfs/status.c
+++ b/src/pmxcfs/status.c
@@ -110,8 +110,7 @@ static memdb_change_t memdb_change_array[] = {
{ .path = "sdn/mac-cache.json" },
{ .path = "sdn/pve-ipam-state.json" },
{ .path = "sdn/dns.cfg" },
- { .path = "sdn/fabrics/openfabric.cfg" },
- { .path = "sdn/fabrics/ospf.cfg" },
+ { .path = "sdn/fabrics.cfg" },
{ .path = "sdn/.running-config" },
{ .path = "virtual-guest/cpu-models.conf" },
{ .path = "virtual-guest/profiles.cfg" },
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-access-control v3 1/1] permissions: add ACL paths for SDN fabrics
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (31 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH pve-cluster v3 1/1] cfs: add fabrics.cfg to observed files Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 01/21] sdn: fix value returned by pending_config Stefan Hanreich
` (41 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
Add permission path /sdn/fabrics/{fabric_id}. There are currently only
SDN-specific permissions for the fabric itself, not the nodes. For
displaying / editing the nodes, the existing permissions Sys.Audit or
Sys.Modify on /nodes/{node} are required, because they are already
used for viewing / editing the network configuration of a node.
The node settings mostly revolve around configuring IPs and network
interfaces on that node, so we decided to stick with the permission
that is already governing that, since it would need to be checked when
editing a node anyway. Otherwise, users with access to a fabric node
could change parts of the network configuration of arbitrary
interfaces that node, circumventing the current permission checks. A
separate, SDN-specific, permission would not add much benefit because
of that.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/AccessControl.pm | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/PVE/AccessControl.pm b/src/PVE/AccessControl.pm
index 1c79656..70864b0 100644
--- a/src/PVE/AccessControl.pm
+++ b/src/PVE/AccessControl.pm
@@ -1273,6 +1273,8 @@ sub check_path {
|/sdn/controllers/[[:alnum:]\_\-]+
|/sdn/dns
|/sdn/dns/[[:alnum:]]+
+ |/sdn/fabrics
+ |/sdn/fabrics/[[:alnum:]]+
|/sdn/ipams
|/sdn/ipams/[[:alnum:]]+
|/sdn/zones
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-network v3 01/21] sdn: fix value returned by pending_config
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (32 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH pve-access-control v3 1/1] permissions: add ACL paths for SDN fabrics Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 02/21] debian: add dependency to proxmox-perl-rs Stefan Hanreich
` (40 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
For special types that were encoded by the encode_value function in
SDN, we returned the encoded value in the API, rather than the actual
value. Since we use the encoded value only for comparison, we need to
return the original value instead of the encoded value.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Network/SDN.pm | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index 68f9e0f..c9c45b1 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -123,7 +123,7 @@ sub pending_config {
if($key eq 'type' || $key eq 'vnet') {
$pending->{$id}->{$key} = $config_value;
} else {
- $pending->{$id}->{"pending"}->{$key} = $config_value if !defined($running_value) || ($config_value ne $running_value);
+ $pending->{$id}->{"pending"}->{$key} = $config_object->{$key} if !defined($running_value) || ($config_value ne $running_value);
}
if(!keys %{$running_object}) {
$pending->{$id}->{state} = "new";
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-network v3 02/21] debian: add dependency to proxmox-perl-rs
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (33 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 01/21] sdn: fix value returned by pending_config Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 03/21] fabrics: add fabrics module Stefan Hanreich
` (39 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
We call perlmod rust functions directly from pve-network.
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
debian/control | 17 ++++++++++-------
1 file changed, 10 insertions(+), 7 deletions(-)
diff --git a/debian/control b/debian/control
index 9ddbb13..41f8ca9 100644
--- a/debian/control
+++ b/debian/control
@@ -4,13 +4,15 @@ Priority: optional
Maintainer: Proxmox Support Team <support@proxmox.com>
Build-Depends: debhelper-compat (= 13),
lintian,
- libfile-slurp-perl <!nocheck>,
- libnet-subnet-perl <!nocheck>,
- libtest-mockmodule-perl <!nocheck>,
- pve-cluster (>= 8.0.10) <!nocheck>,
- pve-firewall (>= 5.1.0~) <!nocheck>,
- pve-doc-generator (>= 5.3-3) <!nocheck>,
- libpve-access-control <!nocheck>,
+ libfile-slurp-perl,
+ libnet-subnet-perl,
+ libpve-rs-perl,
+ libtest-mockmodule-perl,
+ perl,
+ pve-cluster (>= 8.0.10),
+ pve-firewall (>= 5.1.0~),
+ pve-doc-generator (>= 5.3-3),
+ libpve-access-control,
Standards-Version: 4.6.1
Homepage: https://www.proxmox.com
@@ -22,6 +24,7 @@ Depends: libpve-common-perl (>= 5.0-45),
libnet-subnet-perl,
libnet-ip-perl,
libnetaddr-ip-perl,
+ libpve-rs-perl,
${misc:Depends},
${perl:Depends},
Recommends: ifupdown2
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-network v3 03/21] fabrics: add fabrics module
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (34 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 02/21] debian: add dependency to proxmox-perl-rs Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 04/21] refactor: controller: move frr methods into helper Stefan Hanreich
` (38 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
Add a new basic Fabrics module that can be used for reading and
writing the fabrics configuration file.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Network/SDN/Fabrics.pm | 49 ++++++++++++++++++++++++++++++++++
src/PVE/Network/SDN/Makefile | 2 +-
2 files changed, 50 insertions(+), 1 deletion(-)
create mode 100644 src/PVE/Network/SDN/Fabrics.pm
diff --git a/src/PVE/Network/SDN/Fabrics.pm b/src/PVE/Network/SDN/Fabrics.pm
new file mode 100644
index 0000000..77da6ae
--- /dev/null
+++ b/src/PVE/Network/SDN/Fabrics.pm
@@ -0,0 +1,49 @@
+package PVE::Network::SDN::Fabrics;
+
+use strict;
+use warnings;
+
+use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_lock_file cfs_write_file);
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::INotify;
+use PVE::RS::SDN::Fabrics;
+
+cfs_register_file(
+ 'sdn/fabrics.cfg',
+ \&parse_fabrics_config,
+ \&write_fabrics_config,
+);
+
+sub parse_fabrics_config {
+ my ($filename, $raw) = @_;
+ return $raw // '';
+}
+
+sub write_fabrics_config {
+ my ($filename, $config) = @_;
+ return $config // '';
+}
+
+sub config {
+ my ($running) = @_;
+
+ if ($running) {
+ my $running_config = PVE::Network::SDN::running_config();
+
+ # if the config hasn't yet been applied after the introduction of
+ # fabrics then the key does not exist in the running config so we
+ # default to an empty hash
+ my $fabrics_config = $running_config->{fabrics}->{ids} // {};
+ return PVE::RS::SDN::Fabrics->running_config($fabrics_config);
+ }
+
+ my $fabrics_config = cfs_read_file("sdn/fabrics.cfg");
+ return PVE::RS::SDN::Fabrics->config($fabrics_config);
+}
+
+sub write_config {
+ my ($config) = @_;
+ cfs_write_file("sdn/fabrics.cfg", $config->to_raw(), 1);
+}
+
+1;
diff --git a/src/PVE/Network/SDN/Makefile b/src/PVE/Network/SDN/Makefile
index 3e6e5fb..a256642 100644
--- a/src/PVE/Network/SDN/Makefile
+++ b/src/PVE/Network/SDN/Makefile
@@ -1,4 +1,4 @@
-SOURCES=Vnets.pm VnetPlugin.pm Zones.pm Controllers.pm Subnets.pm SubnetPlugin.pm Ipams.pm Dns.pm Dhcp.pm
+SOURCES=Vnets.pm VnetPlugin.pm Zones.pm Controllers.pm Subnets.pm SubnetPlugin.pm Ipams.pm Dns.pm Dhcp.pm Fabrics.pm
PERL5DIR=${DESTDIR}/usr/share/perl5
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-network v3 04/21] refactor: controller: move frr methods into helper
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (35 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 03/21] fabrics: add fabrics module Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 05/21] frr: add new helpers for reloading frr configuration Stefan Hanreich
` (37 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
Up until now the EVPN controller contained all the helper methods as
well as the configuration generation logic for FRR. Since we need to
write FRR configuration with the fabrics as well, move the FRR helper
files into its own FRR module, so they can be used by the EVPN plugin
as well as the future fabrics plugins.
The fact that the EVPN controller was solely responsible for
generating the FRR config also meant, that FRR configuration was only
generated if you had an EVPN controller defined.
In the process of generating an FRR configuration, we used mainly two
formats, which I'll refer to by the following names:
frr_config: This is a perl hash, that loosely resembles the structure
of the FRR configuration file and was later converted into the
raw_config format before writing it.
raw_config: This is an array, that contains strings, where each string
is a line in the FRR configuration. So the finished FRR configuration
consists of all the strings in the array joined by newlines.
Controllers used the frr_config format for generating FRR
configuration. The local configuration in /etc/frr/frr.conf.local also
gets parsed into this format. The fabrics perlmod module, returns the
raw_config format. This was behind the intention to make this split
more clear and handle the FRR config generation in two steps from now
on:
* generate a frr_config in all plugins that utilize that format
* convert it to the raw_config format
* append the configuration obtained via perlmod
* write the finished configuration to frr.conf
This process was already in place, but the distinction wasn't that
clear. During this process I renamed all methods to make clear which
format they accept / return.
Some functions have been split to make them more granular, so we can
use intermediate results. Most namely the
generate_controller_rawconfig function has been split into multiple
functions.
Added documentation to all public FRR functions, so it is clearer
which format they expect, as well as which operations they perform on
the respective passed configurations.
For the future it might make sense to further split the FRR config
generation for zones and vnets into the respective Zone / VNet
Plugins, instead of in the EVPN controller, but this was beyond the
scope of this already quite large patch series.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 292 ---------------
src/PVE/Network/SDN/Frr.pm | 336 ++++++++++++++++++
src/PVE/Network/SDN/Makefile | 2 +-
3 files changed, 337 insertions(+), 293 deletions(-)
create mode 100644 src/PVE/Network/SDN/Frr.pm
diff --git a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
index 5b1a9aa..f9241a0 100644
--- a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
@@ -356,296 +356,4 @@ sub find_isis_controller {
return $res;
}
-sub generate_frr_recurse{
- my ($final_config, $content, $parentkey, $level) = @_;
-
- my $keylist = {};
- $keylist->{'address-family'} = 1;
- $keylist->{router} = 1;
-
- my $exitkeylist = {};
- $exitkeylist->{'address-family'} = 1;
-
- my $simple_exitkeylist = {};
- $simple_exitkeylist->{router} = 1;
-
- # FIXME: make this generic
- my $paddinglevel = undef;
- if ($level == 1 || $level == 2) {
- $paddinglevel = $level - 1;
- } elsif ($level == 3 || $level == 4) {
- $paddinglevel = $level - 2;
- }
-
- my $padding = "";
- $padding = ' ' x ($paddinglevel) if $paddinglevel;
-
- if (ref $content eq 'HASH') {
- foreach my $key (sort keys %$content) {
- next if $key eq 'vrf';
- if ($parentkey && defined($keylist->{$parentkey})) {
- push @{$final_config}, $padding."!";
- push @{$final_config}, $padding."$parentkey $key";
- } elsif ($key ne '' && !defined($keylist->{$key})) {
- push @{$final_config}, $padding."$key";
- }
-
- my $option = $content->{$key};
- generate_frr_recurse($final_config, $option, $key, $level+1);
-
- push @{$final_config}, $padding."exit-$parentkey" if $parentkey && defined($exitkeylist->{$parentkey});
- push @{$final_config}, $padding."exit" if $parentkey && defined($simple_exitkeylist->{$parentkey});
- }
- }
-
- if (ref $content eq 'ARRAY') {
- push @{$final_config}, map { $padding . "$_" } @$content;
- }
-}
-
-sub generate_frr_vrf {
- my ($final_config, $vrfs) = @_;
-
- return if !$vrfs;
-
- my @config = ();
-
- foreach my $id (sort keys %$vrfs) {
- my $vrf = $vrfs->{$id};
- push @config, "!";
- push @config, "vrf $id";
- foreach my $rule (@$vrf) {
- push @config, " $rule";
-
- }
- push @config, "exit-vrf";
- }
-
- push @{$final_config}, @config;
-}
-
-sub generate_frr_simple_list {
- my ($final_config, $rules) = @_;
-
- return if !$rules;
-
- my @config = ();
- push @{$final_config}, "!";
- foreach my $rule (sort @$rules) {
- push @{$final_config}, $rule;
- }
-}
-
-sub generate_frr_interfaces {
- my ($final_config, $interfaces) = @_;
-
- foreach my $k (sort keys %$interfaces) {
- my $iface = $interfaces->{$k};
- push @{$final_config}, "!";
- push @{$final_config}, "interface $k";
- foreach my $rule (sort @$iface) {
- push @{$final_config}, " $rule";
- }
- }
-}
-
-sub generate_frr_routemap {
- my ($final_config, $routemaps) = @_;
-
- foreach my $id (sort keys %$routemaps) {
-
- my $routemap = $routemaps->{$id};
- my $order = 0;
- foreach my $seq (@$routemap) {
- $order++;
- next if !defined($seq->{action});
- my @config = ();
- push @config, "!";
- push @config, "route-map $id $seq->{action} $order";
- my $rule = $seq->{rule};
- push @config, map { " $_" } @$rule;
- push @{$final_config}, @config;
- push @{$final_config}, "exit";
- }
- }
-}
-
-sub generate_frr_list {
- my ($final_config, $lists, $type) = @_;
-
- my $config = [];
-
- for my $id (sort keys %$lists) {
- my $list = $lists->{$id};
-
- for my $seq (sort keys %$list) {
- my $rule = $list->{$seq};
- push @$config, "$type $id seq $seq $rule";
- }
- }
-
- if (@$config > 0) {
- push @{$final_config}, "!", @$config;
- }
-}
-
-sub read_local_frr_config {
- if (-e "/etc/frr/frr.conf.local") {
- return file_get_contents("/etc/frr/frr.conf.local");
- }
-};
-
-sub generate_controller_rawconfig {
- my ($class, $plugin_config, $config) = @_;
-
- my $nodename = PVE::INotify::nodename();
-
- my $final_config = [];
- push @{$final_config}, "frr version 8.5.2";
- push @{$final_config}, "frr defaults datacenter";
- push @{$final_config}, "hostname $nodename";
- push @{$final_config}, "log syslog informational";
- push @{$final_config}, "service integrated-vtysh-config";
- push @{$final_config}, "!";
-
- my $local_conf = read_local_frr_config();
- if ($local_conf) {
- parse_merge_frr_local_config($config, $local_conf);
- }
-
- generate_frr_vrf($final_config, $config->{frr}->{vrf});
- generate_frr_interfaces($final_config, $config->{frr_interfaces});
- generate_frr_recurse($final_config, $config->{frr}, undef, 0);
- generate_frr_list($final_config, $config->{frr_access_list}, "access-list");
- generate_frr_list($final_config, $config->{frr_prefix_list}, "ip prefix-list");
- generate_frr_list($final_config, $config->{frr_prefix_list_v6}, "ipv6 prefix-list");
- generate_frr_simple_list($final_config, $config->{frr_bgp_community_list});
- generate_frr_routemap($final_config, $config->{frr_routemap});
- generate_frr_simple_list($final_config, $config->{frr_ip_protocol});
-
- push @{$final_config}, "!";
- push @{$final_config}, "line vty";
- push @{$final_config}, "!";
-
- my $rawconfig = join("\n", @{$final_config});
-
- return if !$rawconfig;
- return $rawconfig;
-}
-
-sub parse_merge_frr_local_config {
- my ($config, $local_conf) = @_;
-
- my $section = \$config->{""};
- my $router = undef;
- my $routemap = undef;
- my $routemap_config = ();
- my $routemap_action = undef;
-
- while ($local_conf =~ /^\s*(.+?)\s*$/gm) {
- my $line = $1;
- $line =~ s/^\s+|\s+$//g;
-
- if ($line =~ m/^router (.+)$/) {
- $router = $1;
- $section = \$config->{'frr'}->{'router'}->{$router}->{""};
- next;
- } elsif ($line =~ m/^vrf (.+)$/) {
- $section = \$config->{'frr'}->{'vrf'}->{$1};
- next;
- } elsif ($line =~ m/^interface (.+)$/) {
- $section = \$config->{'frr_interfaces'}->{$1};
- next;
- } elsif ($line =~ m/^bgp community-list (.+)$/) {
- push(@{$config->{'frr_bgp_community_list'}}, $line);
- next;
- } elsif ($line =~ m/address-family (.+)$/) {
- $section = \$config->{'frr'}->{'router'}->{$router}->{'address-family'}->{$1};
- next;
- } elsif ($line =~ m/^route-map (.+) (permit|deny) (\d+)/) {
- $routemap = $1;
- $routemap_config = ();
- $routemap_action = $2;
- $section = \$config->{'frr_routemap'}->{$routemap};
- next;
- } elsif ($line =~ m/^access-list (.+) seq (\d+) (.+)$/) {
- $config->{'frr_access_list'}->{$1}->{$2} = $3;
- next;
- } elsif ($line =~ m/^ip prefix-list (.+) seq (\d+) (.*)$/) {
- $config->{'frr_prefix_list'}->{$1}->{$2} = $3;
- next;
- } elsif ($line =~ m/^ipv6 prefix-list (.+) seq (\d+) (.*)$/) {
- $config->{'frr_prefix_list_v6'}->{$1}->{$2} = $3;
- next;
- } elsif($line =~ m/^exit-address-family$/) {
- next;
- } elsif($line =~ m/^exit$/) {
- if($router) {
- $section = \$config->{''};
- $router = undef;
- } elsif($routemap) {
- push(@{$$section}, { rule => $routemap_config, action => $routemap_action });
- $section = \$config->{''};
- $routemap = undef;
- $routemap_action = undef;
- $routemap_config = ();
- }
- next;
- } elsif($line =~ m/!/) {
- next;
- }
-
- next if !$section;
- if($routemap) {
- push(@{$routemap_config}, $line);
- } else {
- push(@{$$section}, $line);
- }
- }
-}
-
-sub write_controller_config {
- my ($class, $plugin_config, $config) = @_;
-
- my $rawconfig = $class->generate_controller_rawconfig($plugin_config, $config);
- return if !$rawconfig;
- return if !-d "/etc/frr";
-
- file_set_contents("/etc/frr/frr.conf", $rawconfig);
-}
-
-sub reload_controller {
- my ($class) = @_;
-
- my $conf_file = "/etc/frr/frr.conf";
- my $bin_path = "/usr/lib/frr/frr-reload.py";
-
- if (!-e $bin_path) {
- log_warn("missing $bin_path. Please install frr-pythontools package");
- return;
- }
-
- run_command(['systemctl', 'enable', '--now', 'frr'])
- if !-e "/etc/systemd/system/multi-user.target.wants/frr.service";
-
- my $err = sub {
- my $line = shift;
- if ($line =~ /ERROR:/) {
- warn "$line \n";
- }
- };
-
- if (-e $conf_file && -e $bin_path) {
- eval {
- run_command([$bin_path, '--stdout', '--reload', $conf_file], errfunc => $err);
- };
- if ($@) {
- warn "frr reload command fail. Restarting frr.";
- eval { run_command(['systemctl', 'restart', 'frr']); };
- }
- }
-}
-
1;
-
-
diff --git a/src/PVE/Network/SDN/Frr.pm b/src/PVE/Network/SDN/Frr.pm
new file mode 100644
index 0000000..20eb4d3
--- /dev/null
+++ b/src/PVE/Network/SDN/Frr.pm
@@ -0,0 +1,336 @@
+package PVE::Network::SDN::Frr;
+
+use strict;
+use warnings;
+
+=head1 NAME
+
+C<PVE::Network::SDN::Frr> - Helper module for FRR
+
+=head1 DESCRIPTION
+
+This module contains helpers for handling the various intermediate FRR
+configuration formats.
+
+We currently mainly use two different intermediate formats throughout the SDN
+module:
+
+=head2 frr config
+
+An frr config represented as a perl hash. The controller plugins generate their
+frr configuration in this format. This format is also used for merging the local
+FRR config (a user-defined configuration file) with the controller-generated
+configuration.
+
+=head2 raw config
+
+This is generated from the frr config. It is an array where every entry is a
+string that is a FRR configuration line.
+
+=cut
+
+use PVE::RESTEnvironment qw(log_warn);
+use PVE::Tools qw(file_get_contents file_set_contents run_command);
+
+=head3 read_local_frr_config
+
+Returns the contents of `/etc/frr/frr.conf.local` as a string if it exists, otherwise undef.
+
+=cut
+
+sub read_local_frr_config {
+ if (-e "/etc/frr/frr.conf.local") {
+ return file_get_contents("/etc/frr/frr.conf.local");
+ }
+};
+
+=head3 to_raw_config(\%frr_config)
+
+Converts a given C<\%frr_config> to the raw config format.
+
+=cut
+
+sub to_raw_config {
+ my ($frr_config) = @_;
+
+ my $raw_config = [];
+
+ generate_frr_vrf($raw_config, $frr_config->{frr}->{vrf});
+ generate_frr_interfaces($raw_config, $frr_config->{frr_interfaces});
+ generate_frr_recurse($raw_config, $frr_config->{frr}, undef, 0);
+ generate_frr_list($raw_config, $frr_config->{frr_access_list}, "access-list");
+ generate_frr_list($raw_config, $frr_config->{frr_prefix_list}, "ip prefix-list");
+ generate_frr_list($raw_config, $frr_config->{frr_prefix_list_v6}, "ipv6 prefix-list");
+ generate_frr_simple_list($raw_config, $frr_config->{frr_bgp_community_list});
+ generate_frr_routemap($raw_config, $frr_config->{frr_routemap});
+ generate_frr_simple_list($raw_config, $frr_config->{frr_ip_protocol});
+
+ return $raw_config;
+}
+
+=head3 raw_config_to_string(\@raw_config)
+
+Converts a given C<\@raw_config> to a string representing a complete frr
+configuration, ready to be written to /etc/frr/frr.conf. If raw_config is empty,
+returns only the FRR config skeleton.
+
+=cut
+
+sub raw_config_to_string {
+ my ($raw_config) = @_;
+
+ my $nodename = PVE::INotify::nodename();
+
+ my @final_config = (
+ "frr version 8.5.2",
+ "frr defaults datacenter",
+ "hostname $nodename",
+ "log syslog informational",
+ "service integrated-vtysh-config",
+ "!",
+ );
+
+ push @final_config, @$raw_config;
+
+ push @final_config, (
+ "!",
+ "line vty",
+ "!",
+ );
+
+ return join("\n", @final_config);
+}
+
+=head3 raw_config_to_string(\@raw_config)
+
+Writes a given C<\@raw_config> to /etc/frr/frr.conf.
+
+=cut
+
+sub write_raw_config {
+ my ($raw_config) = @_;
+
+ return if !-d "/etc/frr";
+ return if !$raw_config;
+
+ file_set_contents("/etc/frr/frr.conf", raw_config_to_string($raw_config));
+
+}
+
+=head3 append_local_config(\%frr_config, $local_config)
+
+Takes an existing C<\%frr_config> and C<$local_config> (as a string). It parses
+the local configuration and appends the values to the existing C<\%frr_config>
+in-place.
+
+=cut
+
+sub append_local_config {
+ my ($frr_config, $local_config) = @_;
+
+ $local_config = read_local_frr_config() if !$local_config;
+ return if !$local_config;
+
+ my $section = \$frr_config->{""};
+ my $router = undef;
+ my $routemap = undef;
+ my $routemap_config = ();
+ my $routemap_action = undef;
+
+ while ($local_config =~ /^\s*(.+?)\s*$/gm) {
+ my $line = $1;
+ $line =~ s/^\s+|\s+$//g;
+
+ if ($line =~ m/^router (.+)$/) {
+ $router = $1;
+ $section = \$frr_config->{'frr'}->{'router'}->{$router}->{""};
+ next;
+ } elsif ($line =~ m/^vrf (.+)$/) {
+ $section = \$frr_config->{'frr'}->{'vrf'}->{$1};
+ next;
+ } elsif ($line =~ m/^interface (.+)$/) {
+ $section = \$frr_config->{'frr_interfaces'}->{$1};
+ next;
+ } elsif ($line =~ m/^bgp community-list (.+)$/) {
+ push(@{$frr_config->{'frr_bgp_community_list'}}, $line);
+ next;
+ } elsif ($line =~ m/address-family (.+)$/) {
+ $section = \$frr_config->{'frr'}->{'router'}->{$router}->{'address-family'}->{$1};
+ next;
+ } elsif ($line =~ m/^route-map (.+) (permit|deny) (\d+)/) {
+ $routemap = $1;
+ $routemap_config = ();
+ $routemap_action = $2;
+ $section = \$frr_config->{'frr_routemap'}->{$routemap};
+ next;
+ } elsif ($line =~ m/^access-list (.+) seq (\d+) (.+)$/) {
+ $frr_config->{'frr_access_list'}->{$1}->{$2} = $3;
+ next;
+ } elsif ($line =~ m/^ip prefix-list (.+) seq (\d+) (.*)$/) {
+ $frr_config->{'frr_prefix_list'}->{$1}->{$2} = $3;
+ next;
+ } elsif ($line =~ m/^ipv6 prefix-list (.+) seq (\d+) (.*)$/) {
+ $frr_config->{'frr_prefix_list_v6'}->{$1}->{$2} = $3;
+ next;
+ } elsif($line =~ m/^exit-address-family$/) {
+ next;
+ } elsif($line =~ m/^exit$/) {
+ if($router) {
+ $section = \$frr_config->{''};
+ $router = undef;
+ } elsif($routemap) {
+ push(@{$$section}, { rule => $routemap_config, action => $routemap_action });
+ $section = \$frr_config->{''};
+ $routemap = undef;
+ $routemap_action = undef;
+ $routemap_config = ();
+ }
+ next;
+ } elsif($line =~ m/!/) {
+ next;
+ }
+
+ next if !$section;
+ if($routemap) {
+ push(@{$routemap_config}, $line);
+ } else {
+ push(@{$$section}, $line);
+ }
+ }
+}
+
+sub generate_frr_recurse {
+ my ($final_config, $content, $parentkey, $level) = @_;
+
+ my $keylist = {};
+ $keylist->{'address-family'} = 1;
+ $keylist->{router} = 1;
+
+ my $exitkeylist = {};
+ $exitkeylist->{'address-family'} = 1;
+
+ my $simple_exitkeylist = {};
+ $simple_exitkeylist->{router} = 1;
+
+ # FIXME: make this generic
+ my $paddinglevel = undef;
+ if ($level == 1 || $level == 2) {
+ $paddinglevel = $level - 1;
+ } elsif ($level == 3 || $level == 4) {
+ $paddinglevel = $level - 2;
+ }
+
+ my $padding = "";
+ $padding = ' ' x ($paddinglevel) if $paddinglevel;
+
+ if (ref $content eq 'HASH') {
+ foreach my $key (sort keys %$content) {
+ next if $key eq 'vrf';
+ if ($parentkey && defined($keylist->{$parentkey})) {
+ push @{$final_config}, $padding."!";
+ push @{$final_config}, $padding."$parentkey $key";
+ } elsif ($key ne '' && !defined($keylist->{$key})) {
+ push @{$final_config}, $padding."$key";
+ }
+
+ my $option = $content->{$key};
+ generate_frr_recurse($final_config, $option, $key, $level+1);
+
+ push @{$final_config}, $padding."exit-$parentkey" if $parentkey && defined($exitkeylist->{$parentkey});
+ push @{$final_config}, $padding."exit" if $parentkey && defined($simple_exitkeylist->{$parentkey});
+ }
+ }
+
+ if (ref $content eq 'ARRAY') {
+ push @{$final_config}, map { $padding . "$_" } @$content;
+ }
+}
+
+sub generate_frr_vrf {
+ my ($final_config, $vrfs) = @_;
+
+ return if !$vrfs;
+
+ my @config = ();
+
+ foreach my $id (sort keys %$vrfs) {
+ my $vrf = $vrfs->{$id};
+ push @config, "!";
+ push @config, "vrf $id";
+ foreach my $rule (@$vrf) {
+ push @config, " $rule";
+
+ }
+ push @config, "exit-vrf";
+ }
+
+ push @{$final_config}, @config;
+}
+
+sub generate_frr_simple_list {
+ my ($final_config, $rules) = @_;
+
+ return if !$rules;
+
+ my @config = ();
+ push @{$final_config}, "!";
+ foreach my $rule (sort @$rules) {
+ push @{$final_config}, $rule;
+ }
+}
+
+sub generate_frr_list {
+ my ($final_config, $lists, $type) = @_;
+
+ my $config = [];
+
+ for my $id (sort keys %$lists) {
+ my $list = $lists->{$id};
+
+ for my $seq (sort keys %$list) {
+ my $rule = $list->{$seq};
+ push @$config, "$type $id seq $seq $rule";
+ }
+ }
+
+ if (@$config > 0) {
+ push @{$final_config}, "!", @$config;
+ }
+}
+
+
+sub generate_frr_interfaces {
+ my ($final_config, $interfaces) = @_;
+
+ foreach my $k (sort keys %$interfaces) {
+ my $iface = $interfaces->{$k};
+ push @{$final_config}, "!";
+ push @{$final_config}, "interface $k";
+ foreach my $rule (sort @$iface) {
+ push @{$final_config}, " $rule";
+ }
+ }
+}
+
+sub generate_frr_routemap {
+ my ($final_config, $routemaps) = @_;
+
+ foreach my $id (sort keys %$routemaps) {
+
+ my $routemap = $routemaps->{$id};
+ my $order = 0;
+ foreach my $seq (@$routemap) {
+ $order++;
+ next if !defined($seq->{action});
+ my @config = ();
+ push @config, "!";
+ push @config, "route-map $id $seq->{action} $order";
+ my $rule = $seq->{rule};
+ push @config, map { " $_" } @$rule;
+ push @{$final_config}, @config;
+ push @{$final_config}, "exit";
+ }
+ }
+}
+
+1;
diff --git a/src/PVE/Network/SDN/Makefile b/src/PVE/Network/SDN/Makefile
index a256642..d1ffef9 100644
--- a/src/PVE/Network/SDN/Makefile
+++ b/src/PVE/Network/SDN/Makefile
@@ -1,4 +1,4 @@
-SOURCES=Vnets.pm VnetPlugin.pm Zones.pm Controllers.pm Subnets.pm SubnetPlugin.pm Ipams.pm Dns.pm Dhcp.pm Fabrics.pm
+SOURCES=Vnets.pm VnetPlugin.pm Zones.pm Controllers.pm Subnets.pm SubnetPlugin.pm Ipams.pm Dns.pm Dhcp.pm Fabrics.pm Frr.pm
PERL5DIR=${DESTDIR}/usr/share/perl5
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-network v3 05/21] frr: add new helpers for reloading frr configuration
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (36 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 04/21] refactor: controller: move frr methods into helper Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 06/21] controllers: define new api for frr config generation Stefan Hanreich
` (36 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
The reload and restart parts of the original reload_controller_config
have been split into two functions, in order to make error handling
in the new apply function easier.
The new apply function tries to reload via frr-reload.py and if that
fails, it falls back to restarting the frr service.
Since frr-reload.py does *not* start / stop daemons that have been
added / remove to /etc/frr/daemons, we add a new parameter that can be
used to restart the frr service instead of just using frr-reload.py.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Network/SDN/Frr.pm | 65 ++++++++++++++++++++++++++++++++++++++
1 file changed, 65 insertions(+)
diff --git a/src/PVE/Network/SDN/Frr.pm b/src/PVE/Network/SDN/Frr.pm
index 20eb4d3..871d800 100644
--- a/src/PVE/Network/SDN/Frr.pm
+++ b/src/PVE/Network/SDN/Frr.pm
@@ -44,6 +44,71 @@ sub read_local_frr_config {
}
};
+my $FRR_CONFIG_FILE = "/etc/frr/frr.conf";
+
+=head3 apply()
+
+Tries to reload FRR with the frr-reload.py script from frr-pythontools. If that
+isn't installed or doesn't work it falls back to restarting the systemd frr
+service. If C<$force_restart> is set, then the FRR daemon will be restarted,
+without trying to reload it first.
+
+=cut
+
+sub apply {
+ my ($force_restart) = @_;
+
+ if (!-e $FRR_CONFIG_FILE) {
+ log_warn("$FRR_CONFIG_FILE is not present.");
+ return;
+ }
+
+ run_command(['systemctl', 'enable', '--now', 'frr'])
+ if !-e "/etc/systemd/system/multi-user.target.wants/frr.service";
+
+ if (!$force_restart) {
+ eval { reload() };
+ return if !$@;
+
+ warn "reloading frr configuration failed: $@";
+ warn "trying to restart frr instead";
+ }
+
+ eval { restart() };
+ warn "restarting frr failed: $@" if $@;
+}
+
+sub reload {
+ my $bin_path = "/usr/lib/frr/frr-reload.py";
+
+ if (!-e $bin_path) {
+ die "missing $bin_path. Please install the frr-pythontools package";
+ }
+
+ my $err = sub {
+ my $line = shift;
+ warn "$line \n";
+ };
+
+ run_command([$bin_path, '--stdout', '--reload', $FRR_CONFIG_FILE], errfunc => $err);
+}
+
+sub restart {
+ # script invoked by the frr systemd service
+ my $bin_path = "/usr/lib/frr/frrinit.sh";
+
+ if (!-e $bin_path) {
+ die "missing $bin_path. Please install the frr package";
+ }
+
+ my $err = sub {
+ my $line = shift;
+ warn "$line \n";
+ };
+
+ run_command(['systemctl', 'restart', 'frr'], errfunc => $err);
+}
+
=head3 to_raw_config(\%frr_config)
Converts a given C<\%frr_config> to the raw config format.
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-network v3 06/21] controllers: define new api for frr config generation
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (37 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 05/21] frr: add new helpers for reloading frr configuration Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 07/21] sdn: add frr config generation helpers Stefan Hanreich
` (35 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
With the changes to how we handle the frr config generation,
controllers are now no longer responsible for serializing and writing
the FRR configuration. Instead, we pass the existing frr_config perl
hash to every controller, where controllers append their respective
configuration.
This requires a few changes in the controller API, so that they now
append to a perl hash, instead of directly writing their own
configuration to the file, which is now handled externally by the SDN
module.
We also remove the respective methods in the EvpnPlugin that were
previously responsible for serializing and writing the FRR
configuration, since they have been moved to the Frr helper module
instead.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Network/SDN/Controllers.pm | 67 +++----------------
src/PVE/Network/SDN/Controllers/BgpPlugin.pm | 21 +-----
src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 6 +-
src/PVE/Network/SDN/Controllers/IsisPlugin.pm | 21 +-----
src/PVE/Network/SDN/Controllers/Plugin.pm | 31 +--------
5 files changed, 19 insertions(+), 127 deletions(-)
diff --git a/src/PVE/Network/SDN/Controllers.pm b/src/PVE/Network/SDN/Controllers.pm
index 9e8f3aa..788bfcc 100644
--- a/src/PVE/Network/SDN/Controllers.pm
+++ b/src/PVE/Network/SDN/Controllers.pm
@@ -79,12 +79,12 @@ sub read_etc_network_interfaces {
return $interfaces_config;
}
-sub generate_controller_config {
+sub generate_frr_config {
+ my ($frr_config, $sdn_config) = @_;
- my $cfg = PVE::Network::SDN::running_config();
- my $vnet_cfg = $cfg->{vnets};
- my $zone_cfg = $cfg->{zones};
- my $controller_cfg = $cfg->{controllers};
+ my $vnet_cfg = $sdn_config->{vnets};
+ my $zone_cfg = $sdn_config->{zones};
+ my $controller_cfg = $sdn_config->{controllers};
return if !$vnet_cfg && !$zone_cfg && !$controller_cfg;
@@ -101,13 +101,10 @@ sub generate_controller_config {
}
}
- # generate configuration
- my $config = {};
-
foreach my $id (sort keys %{$controller_cfg->{ids}}) {
my $plugin_config = $controller_cfg->{ids}->{$id};
my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($plugin_config->{type});
- $plugin->generate_controller_config($plugin_config, $controller_cfg, $id, $uplinks, $config);
+ $plugin->generate_frr_config($plugin_config, $controller_cfg, $id, $uplinks, $frr_config);
}
foreach my $id (sort keys %{$zone_cfg->{ids}}) {
@@ -117,7 +114,7 @@ sub generate_controller_config {
my $controller = $controller_cfg->{ids}->{$controllerid};
if ($controller) {
my $controller_plugin = PVE::Network::SDN::Controllers::Plugin->lookup($controller->{type});
- $controller_plugin->generate_controller_zone_config($plugin_config, $controller, $controller_cfg, $id, $uplinks, $config);
+ $controller_plugin->generate_zone_frr_config($plugin_config, $controller, $controller_cfg, $id, $uplinks, $frr_config);
}
}
@@ -132,57 +129,11 @@ sub generate_controller_config {
my $controller = $controller_cfg->{ids}->{$controllerid};
if ($controller) {
my $controller_plugin = PVE::Network::SDN::Controllers::Plugin->lookup($controller->{type});
- $controller_plugin->generate_controller_vnet_config($plugin_config, $controller, $zone, $zoneid, $id, $config);
+ $controller_plugin->generate_vnet_frr_config($plugin_config, $controller, $zone, $zoneid, $id, $frr_config);
}
}
- return $config;
-}
-
-
-sub reload_controller {
-
- my $cfg = PVE::Network::SDN::running_config();
- my $controller_cfg = $cfg->{controllers};
-
- return if !$controller_cfg;
-
- foreach my $id (keys %{$controller_cfg->{ids}}) {
- my $plugin_config = $controller_cfg->{ids}->{$id};
- my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($plugin_config->{type});
- $plugin->reload_controller();
- }
-}
-
-sub generate_controller_rawconfig {
- my ($config) = @_;
-
- my $cfg = PVE::Network::SDN::running_config();
- my $controller_cfg = $cfg->{controllers};
- return if !$controller_cfg;
-
- my $rawconfig = "";
- foreach my $id (keys %{$controller_cfg->{ids}}) {
- my $plugin_config = $controller_cfg->{ids}->{$id};
- my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($plugin_config->{type});
- $rawconfig .= $plugin->generate_controller_rawconfig($plugin_config, $config);
- }
- return $rawconfig;
-}
-
-sub write_controller_config {
- my ($config) = @_;
-
- my $cfg = PVE::Network::SDN::running_config();
- my $controller_cfg = $cfg->{controllers};
- return if !$controller_cfg;
-
- foreach my $id (keys %{$controller_cfg->{ids}}) {
- my $plugin_config = $controller_cfg->{ids}->{$id};
- my $plugin = PVE::Network::SDN::Controllers::Plugin->lookup($plugin_config->{type});
- $plugin->write_controller_config($plugin_config, $config);
- }
+ return $frr_config;
}
1;
-
diff --git a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
index 53963e5..3b21cad 100644
--- a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
@@ -53,7 +53,7 @@ sub options {
}
# Plugin implementation
-sub generate_controller_config {
+sub generate_frr_config {
my ($class, $plugin_config, $controller, $id, $uplinks, $config) = @_;
my @peers;
@@ -132,7 +132,7 @@ sub generate_controller_config {
return $config;
}
-sub generate_controller_zone_config {
+sub generate_zone_frr_config {
my ($class, $plugin_config, $controller, $controller_cfg, $id, $uplinks, $config) = @_;
}
@@ -164,21 +164,4 @@ sub on_update_hook {
}
}
-sub generate_controller_rawconfig {
- my ($class, $plugin_config, $config) = @_;
- return "";
-}
-
-sub write_controller_config {
- my ($class, $plugin_config, $config) = @_;
- return;
-}
-
-sub reload_controller {
- my ($class) = @_;
- return;
-}
-
1;
-
-
diff --git a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
index f9241a0..bde331f 100644
--- a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
@@ -41,7 +41,7 @@ sub options {
}
# Plugin implementation
-sub generate_controller_config {
+sub generate_frr_config {
my ($class, $plugin_config, $controller_cfg, $id, $uplinks, $config) = @_;
my @peers;
@@ -119,7 +119,7 @@ sub generate_controller_config {
return $config;
}
-sub generate_controller_zone_config {
+sub generate_zone_frr_config {
my ($class, $plugin_config, $controller, $controller_cfg, $id, $uplinks, $config) = @_;
my $local_node = PVE::INotify::nodename();
@@ -279,7 +279,7 @@ sub generate_controller_zone_config {
return $config;
}
-sub generate_controller_vnet_config {
+sub generate_vnet_frr_config {
my ($class, $plugin_config, $controller, $zone, $zoneid, $vnetid, $config) = @_;
my $exitnodes = $zone->{'exitnodes'};
diff --git a/src/PVE/Network/SDN/Controllers/IsisPlugin.pm b/src/PVE/Network/SDN/Controllers/IsisPlugin.pm
index 97c6876..ace19aa 100644
--- a/src/PVE/Network/SDN/Controllers/IsisPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/IsisPlugin.pm
@@ -55,7 +55,7 @@ sub options {
}
# Plugin implementation
-sub generate_controller_config {
+sub generate_frr_config {
my ($class, $plugin_config, $controller, $id, $uplinks, $config) = @_;
my $isis_ifaces = $plugin_config->{'isis-ifaces'};
@@ -87,7 +87,7 @@ sub generate_controller_config {
return $config;
}
-sub generate_controller_zone_config {
+sub generate_zone_frr_config {
my ($class, $plugin_config, $controller, $controller_cfg, $id, $uplinks, $config) = @_;
}
@@ -113,21 +113,4 @@ sub on_update_hook {
}
}
-sub generate_controller_rawconfig {
- my ($class, $plugin_config, $config) = @_;
- return "";
-}
-
-sub write_controller_config {
- my ($class, $plugin_config, $config) = @_;
- return;
-}
-
-sub reload_controller {
- my ($class) = @_;
- return;
-}
-
1;
-
-
diff --git a/src/PVE/Network/SDN/Controllers/Plugin.pm b/src/PVE/Network/SDN/Controllers/Plugin.pm
index d6ffc5f..26beff3 100644
--- a/src/PVE/Network/SDN/Controllers/Plugin.pm
+++ b/src/PVE/Network/SDN/Controllers/Plugin.pm
@@ -63,48 +63,23 @@ sub parse_section_header {
return undef;
}
-sub generate_sdn_config {
- my ($class, $plugin_config, $node, $data, $ctime) = @_;
-
- die "please implement inside plugin";
-}
-
-sub generate_controller_config {
+sub generate_frr_config {
my ($class, $plugin_config, $controller_cfg, $id, $uplinks, $config) = @_;
die "please implement inside plugin";
}
-
-sub generate_controller_zone_config {
+sub generate_zone_frr_config {
my ($class, $plugin_config, $controller, $controller_cfg, $id, $uplinks, $config) = @_;
die "please implement inside plugin";
}
-sub generate_controller_vnet_config {
+sub generate_vnet_frr_config {
my ($class, $plugin_config, $controller, $zoneid, $vnetid, $config) = @_;
}
-sub generate_controller_rawconfig {
- my ($class, $plugin_config, $config) = @_;
-
- die "please implement inside plugin";
-}
-
-sub write_controller_config {
- my ($class, $plugin_config, $config) = @_;
-
- die "please implement inside plugin";
-}
-
-sub controller_reload {
- my ($class) = @_;
-
- die "please implement inside plugin";
-}
-
sub on_delete_hook {
my ($class, $controllerid, $zone_cfg) = @_;
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-network v3 07/21] sdn: add frr config generation helpers
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (38 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 06/21] controllers: define new api for frr config generation Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 08/21] sdn: api: add check for rewriting frr configuration Stefan Hanreich
` (34 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
Adds a new method to the SDN module that is responsible for generating
and writing the FRR configuration for all SDN plugins combined. It
utilizes the newly introduced FRR helper as well as the newly
introduced API for the controllers to generate an frr_config instead
of generating the configuration in the controller directly. It can
also reload the FRR daemon.
Change the tests to use this new API as well, so they use the new
methods for generating the frr configuration. They previously used a
different code-path for generating the FRR config compared to the
actual worker task, so this also ensures that tests validate the
configuration that *actually* gets generated.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Network/SDN.pm | 45 ++++++++++++++++++++++++++++++----
src/PVE/Network/SDN/Fabrics.pm | 13 ++++++++++
src/test/run_test_zones.pl | 9 +++----
3 files changed, 57 insertions(+), 10 deletions(-)
diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index c9c45b1..382147f 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -20,6 +20,8 @@ use PVE::Network::SDN::Zones;
use PVE::Network::SDN::Controllers;
use PVE::Network::SDN::Subnets;
use PVE::Network::SDN::Dhcp;
+use PVE::Network::SDN::Frr;
+use PVE::Network::SDN::Fabrics;
my $running_cfg = "sdn/.running-config";
@@ -226,13 +228,46 @@ sub generate_zone_config {
PVE::Network::SDN::Zones::write_etc_network_config($raw_config);
}
-sub generate_controller_config {
- my ($reload) = @_;
+=head3 generate_frr_raw_config(\%running_config, \%fabric_config)
+
+Generates the raw frr config (as documented in the C<PVE::Network::SDN::Frr>
+module) for all SDN plugins combined.
+
+If provided, uses the passed C<\%running_config> und C<\%fabric_config> to avoid
+re-parsing and re-reading both configurations. If not provided, this function
+will obtain them via the SDN and SDN::Fabrics modules and then generate the FRR
+configuration.
+
+=cut
+
+sub generate_frr_raw_config {
+ my ($running_config, $fabric_config) = @_;
+
+ $running_config = PVE::Network::SDN::running_config() if !$running_config;
+ $fabric_config = PVE::Network::SDN::Fabrics::config(1) if !$fabric_config;
+
+ my $frr_config = {};
+ PVE::Network::SDN::Controllers::generate_frr_config($frr_config, $running_config);
+ PVE::Network::SDN::Frr::append_local_config($frr_config);
+
+ my $raw_config = PVE::Network::SDN::Frr::to_raw_config($frr_config);
+
+ my $fabrics_config = PVE::Network::SDN::Fabrics::generate_frr_raw_config($fabric_config);
+ push @$raw_config, @$fabrics_config;
+
+ return $raw_config;
+}
+
+sub generate_frr_config {
+ my ($apply) = @_;
+
+ my $running_config = PVE::Network::SDN::running_config();
+ my $fabric_config = PVE::Network::SDN::Fabrics::config(1);
- my $raw_config = PVE::Network::SDN::Controllers::generate_controller_config();
- PVE::Network::SDN::Controllers::write_controller_config($raw_config);
+ my $raw_config = PVE::Network::SDN::generate_frr_raw_config($running_config, $fabric_config);
+ PVE::Network::SDN::Frr::write_raw_config($raw_config);
- PVE::Network::SDN::Controllers::reload_controller() if $reload;
+ PVE::Network::SDN::Frr::apply() if $apply;
}
sub generate_dhcp_config {
diff --git a/src/PVE/Network/SDN/Fabrics.pm b/src/PVE/Network/SDN/Fabrics.pm
index 77da6ae..0a98f59 100644
--- a/src/PVE/Network/SDN/Fabrics.pm
+++ b/src/PVE/Network/SDN/Fabrics.pm
@@ -46,4 +46,17 @@ sub write_config {
cfs_write_file("sdn/fabrics.cfg", $config->to_raw(), 1);
}
+sub generate_frr_raw_config {
+ my ($fabric_config) = @_;
+
+ my @raw_config = ();
+
+ my $nodename = PVE::INotify::nodename();
+
+ my $frr_config = $fabric_config->get_frr_raw_config($nodename);
+ push @raw_config, @$frr_config if @$frr_config;
+
+ return \@raw_config;
+}
+
1;
diff --git a/src/test/run_test_zones.pl b/src/test/run_test_zones.pl
index e506bea..4137da6 100755
--- a/src/test/run_test_zones.pl
+++ b/src/test/run_test_zones.pl
@@ -140,18 +140,17 @@ foreach my $test (@tests) {
if ($sdn_config->{controllers}) {
my $expected = read_file("./$test/expected_controller_config");
- my $controller_rawconfig = "";
+ my $config = "";
eval {
- my $config = PVE::Network::SDN::Controllers::generate_controller_config();
- $controller_rawconfig =
- PVE::Network::SDN::Controllers::generate_controller_rawconfig($config);
+ my $raw_config = PVE::Network::SDN::generate_frr_raw_config();
+ $config = PVE::Network::SDN::Frr::raw_config_to_string($raw_config);
};
if (my $err = $@) {
diag("got unexpected error - $err");
fail($name);
} else {
- is($controller_rawconfig, $expected, $name);
+ is($config, $expected, $name);
}
}
}
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-network v3 08/21] sdn: api: add check for rewriting frr configuration
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (39 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 07/21] sdn: add frr config generation helpers Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 09/21] test: isis: add test for standalone configuration Stefan Hanreich
` (33 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
With the old FRR config generation logic, we never wrote an empty FRR
configuration if all controllers got deleted. This meant that deleting
all controllers still left the previous FRR configuration on the
nodes, never disabling BGP / IS-IS. The new logic now writes an empty
configuration if there is no controller / fabric configured, fixing
this behavior. This has a side effect for users with an existing FRR
configuration not managed by SDN, but utilizing other SDN features
(zones, vnets, ...). Their manual FRR configuration would get
overwritten when applying any SDN configuration. This is particularly
an issue with full-mesh Ceph setups, that were set up according to our
Wiki guide [1]. User with such a full-mesh setup could get their FRR
configuration overwritten when using unrelated SDN features.
Since we call the API endpoint in pve-manager for generating and
writing configuration files, we cannot directly prevent the FRR
configuration from being written in the SDN API call. Instead a new
parameter, skip_frr, has been added to the endpoint in pve-manager,
that skips writing the FRR configuration. We skip writing the FRR
configuration if neither the previous SDN configuration, nor the new
SDN configuration contains an entity that requires writing FRR
configuration. This should minimize the impact of the change to the
FRR config generation on existing FRR setups.
[1] https://pve.proxmox.com/mediawiki/index.php?title=Full_Mesh_Network_for_Ceph_Server&oldid=12146
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN.pm | 15 ++++++++++++---
src/PVE/Network/SDN.pm | 21 +++++++++++++++++++++
2 files changed, 33 insertions(+), 3 deletions(-)
diff --git a/src/PVE/API2/Network/SDN.pm b/src/PVE/API2/Network/SDN.pm
index d216e48..910f89a 100644
--- a/src/PVE/API2/Network/SDN.pm
+++ b/src/PVE/API2/Network/SDN.pm
@@ -82,12 +82,17 @@ __PACKAGE__->register_method({
}});
my $create_reload_network_worker = sub {
- my ($nodename) = @_;
+ my ($nodename, $skip_frr) = @_;
+
+ my @command = ('pvesh', 'set', "/nodes/$nodename/network");
+ if ($skip_frr) {
+ push(@command, '--skip_frr');
+ }
# FIXME: how to proxy to final node ?
my $upid;
print "$nodename: reloading network config\n";
- run_command(['pvesh', 'set', "/nodes/$nodename/network"], outfunc => sub {
+ run_command(\@command, outfunc => sub {
my $line = shift;
if ($line =~ /["']?(UPID:[^\s"']+)["']?$/) {
$upid = $1;
@@ -120,14 +125,18 @@ __PACKAGE__->register_method ({
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
+ my $previous_config_has_frr = PVE::Network::SDN::running_config_has_frr();
PVE::Network::SDN::commit_config();
+ my $new_config_has_frr = PVE::Network::SDN::running_config_has_frr();
+ my $skip_frr = !($previous_config_has_frr || $new_config_has_frr);
+
my $code = sub {
$rpcenv->{type} = 'priv'; # to start tasks in background
PVE::Cluster::check_cfs_quorum();
my $nodelist = PVE::Cluster::get_nodelist();
for my $node (@$nodelist) {
- my $pid = eval { $create_reload_network_worker->($node) };
+ my $pid = eval { $create_reload_network_worker->($node, $skip_frr) };
warn $@ if $@;
}
diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index 382147f..1ab2e4d 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -89,6 +89,27 @@ sub running_config {
return cfs_read_file($running_cfg);
}
+=head3 running_config_has_frr(\%running_config)
+
+Determines whether C<\%running_config> contains any entities that generate an
+FRR configuration. This is used by pve-manager to determine whether a rewrite of
+the FRR configuration is required or not.
+
+If C<\%running_config> is not provided, it will query the current running
+configuration and then evaluate it.
+
+=cut
+
+sub running_config_has_frr {
+ my $running_config = PVE::Network::SDN::running_config();
+
+ # both can be empty if the SDN configuration was never applied
+ my $controllers = $running_config->{controllers}->{ids} // {};
+ my $fabrics = $running_config->{fabrics}->{ids} // {};
+
+ return %$controllers || %$fabrics;
+}
+
sub pending_config {
my ($running_cfg, $cfg, $type) = @_;
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-network v3 09/21] test: isis: add test for standalone configuration
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (40 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 08/21] sdn: api: add check for rewriting frr configuration Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 10/21] sdn: frr: add daemon status to frr helper Stefan Hanreich
` (32 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
With how the config generation worked before, it was not possible to
create a standalone isis controller, since the FRR config was only
generated with an existing EVPN controller. Since each controller is
now responsible for creating its own configuration, it is possible to
create a standalone isis controller without having any evpn controller
configured. Add a test that covers that scenario.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
.../expected_controller_config | 22 +++++++++++++++++++
.../isis_standalone/expected_sdn_interfaces | 1 +
.../zones/evpn/isis_standalone/interfaces | 12 ++++++++++
.../zones/evpn/isis_standalone/sdn_config | 21 ++++++++++++++++++
4 files changed, 56 insertions(+)
create mode 100644 src/test/zones/evpn/isis_standalone/expected_controller_config
create mode 100644 src/test/zones/evpn/isis_standalone/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/isis_standalone/interfaces
create mode 100644 src/test/zones/evpn/isis_standalone/sdn_config
diff --git a/src/test/zones/evpn/isis_standalone/expected_controller_config b/src/test/zones/evpn/isis_standalone/expected_controller_config
new file mode 100644
index 0000000..5c9bf1a
--- /dev/null
+++ b/src/test/zones/evpn/isis_standalone/expected_controller_config
@@ -0,0 +1,22 @@
+frr version 8.5.2
+frr defaults datacenter
+hostname localhost
+log syslog informational
+service integrated-vtysh-config
+!
+!
+interface eth0
+ ip router isis isis1
+!
+interface eth1
+ ip router isis isis1
+!
+router isis isis1
+ net 47.0023.0000.0000.0000.0000.0000.0000.1900.0004.00
+ redistribute ipv4 connected level-1
+ redistribute ipv6 connected level-1
+ log-adjacency-changes
+exit
+!
+line vty
+!
\ No newline at end of file
diff --git a/src/test/zones/evpn/isis_standalone/expected_sdn_interfaces b/src/test/zones/evpn/isis_standalone/expected_sdn_interfaces
new file mode 100644
index 0000000..edc8ff9
--- /dev/null
+++ b/src/test/zones/evpn/isis_standalone/expected_sdn_interfaces
@@ -0,0 +1 @@
+#version:1
diff --git a/src/test/zones/evpn/isis_standalone/interfaces b/src/test/zones/evpn/isis_standalone/interfaces
new file mode 100644
index 0000000..41ae25f
--- /dev/null
+++ b/src/test/zones/evpn/isis_standalone/interfaces
@@ -0,0 +1,12 @@
+auto vmbr0
+iface vmbr0 inet static
+ address 192.168.0.1/24
+ gateway 192.168.0.254
+ bridge-ports eth0
+ bridge-stp off
+ bridge-fd 0
+
+auto dummy1
+iface dummy1 inet static
+ address 10.0.0.1/32
+ link-type dummy
\ No newline at end of file
diff --git a/src/test/zones/evpn/isis_standalone/sdn_config b/src/test/zones/evpn/isis_standalone/sdn_config
new file mode 100644
index 0000000..331051f
--- /dev/null
+++ b/src/test/zones/evpn/isis_standalone/sdn_config
@@ -0,0 +1,21 @@
+{
+ version => 1,
+ vnets => {
+ },
+ zones => {
+ },
+ controllers => {
+ ids => {
+ localhost => {
+ type => "isis",
+ 'isis-domain' => 'isis1',
+ 'isis-ifaces' => 'eth1,eth0',
+ 'isis-net' => "47.0023.0000.0000.0000.0000.0000.0000.1900.0004.00",
+ loopback => 'dummy1',
+ node => "localhost",
+ },
+ },
+ },
+ subnets => {
+ },
+}
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-network v3 10/21] sdn: frr: add daemon status to frr helper
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (41 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 09/21] test: isis: add test for standalone configuration Stefan Hanreich
@ 2025-05-22 16:16 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 11/21] sdn: commit fabrics config to running configuration Stefan Hanreich
` (31 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:16 UTC (permalink / raw)
To: pve-devel
Add functions that allow reading and manipulating values in the
/etc/frr/daemons file. We need this for en/disabling daemons depending
on which fabric types are configured. We enable daemons dynamically,
depending on the currently configured fabrics. If a daemon is enabled
but all fabrics using it get deleted, we disable them as well.
The helper works by iterating over the lines of the daemons file from
FRR, parsing the key and checking if the key is managed by the SDN
configuration, then sets it. As a safeguard, keys that can be changed
by SDN have to be explicitly configured in the respective hash of the
Frr module.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Network/SDN.pm | 18 +++++++++-
src/PVE/Network/SDN/Fabrics.pm | 15 ++++++++
src/PVE/Network/SDN/Frr.pm | 64 ++++++++++++++++++++++++++++++++++
3 files changed, 96 insertions(+), 1 deletion(-)
diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index 1ab2e4d..8c9ce8c 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -279,16 +279,32 @@ sub generate_frr_raw_config {
return $raw_config;
}
+=head3 get_frr_daemon_status(\%fabric_config)
+
+Returns a hash that indicates which FRR daemons, that are managed by SDN, should
+be enabled / disabled.
+
+=cut
+
+sub get_frr_daemon_status {
+ my ($fabric_config) = @_;
+
+ return PVE::Network::SDN::Fabrics::get_frr_daemon_status($fabric_config);
+}
+
sub generate_frr_config {
my ($apply) = @_;
my $running_config = PVE::Network::SDN::running_config();
my $fabric_config = PVE::Network::SDN::Fabrics::config(1);
+ my $daemon_status = PVE::Network::SDN::get_frr_daemon_status($fabric_config);
+ my $needs_restart = PVE::Network::SDN::Frr::set_daemon_status($daemon_status, 1);
+
my $raw_config = PVE::Network::SDN::generate_frr_raw_config($running_config, $fabric_config);
PVE::Network::SDN::Frr::write_raw_config($raw_config);
- PVE::Network::SDN::Frr::apply() if $apply;
+ PVE::Network::SDN::Frr::apply($needs_restart) if $apply;
}
sub generate_dhcp_config {
diff --git a/src/PVE/Network/SDN/Fabrics.pm b/src/PVE/Network/SDN/Fabrics.pm
index 0a98f59..4950a88 100644
--- a/src/PVE/Network/SDN/Fabrics.pm
+++ b/src/PVE/Network/SDN/Fabrics.pm
@@ -46,6 +46,21 @@ sub write_config {
cfs_write_file("sdn/fabrics.cfg", $config->to_raw(), 1);
}
+sub get_frr_daemon_status {
+ my ($fabric_config) = @_;
+
+ my $daemon_status = {};
+ my $nodename = PVE::INotify::nodename();
+
+ my $enabled_daemons = $fabric_config->enabled_daemons($nodename);
+
+ for my $daemon (@$enabled_daemons) {
+ $daemon_status->{$daemon} = 1;
+ }
+
+ return $daemon_status;
+}
+
sub generate_frr_raw_config {
my ($fabric_config) = @_;
diff --git a/src/PVE/Network/SDN/Frr.pm b/src/PVE/Network/SDN/Frr.pm
index 871d800..4950bf5 100644
--- a/src/PVE/Network/SDN/Frr.pm
+++ b/src/PVE/Network/SDN/Frr.pm
@@ -109,6 +109,70 @@ sub restart {
run_command(['systemctl', 'restart', 'frr'], errfunc => $err);
}
+my $SDN_DAEMONS_DEFAULT = {
+ ospfd => 0,
+ fabricd => 0,
+};
+
+=head3 set_daemon_status(\%daemons, $set_default)
+
+Sets the status of all daemons supplied in C<\%daemons>. This only works for
+daemons managed by SDN, as indicated in the C<$SDN_DAEMONS_DEFAULT> constant. If
+a daemon is supplied that isn't managed by SDN then this command will fail. If
+C<$set_default> is set, then additionally all sdn-managed daemons that are
+missing in C<\%daemons> are reset to their default value. It returns whether the
+status of any daemons has changed, which indicates that a restart of the daemon
+is required, rather than only a reload.
+
+=cut
+
+sub set_daemon_status {
+ my ($daemon_status, $set_default) = @_;
+
+ my $daemons_file = "/etc/frr/daemons";
+ die "daemons file does not exist" if !-e $daemons_file;
+
+ for my $daemon (keys %$daemon_status) {
+ die "$daemon is not SDN managed" if !defined $SDN_DAEMONS_DEFAULT->{$daemon};
+ }
+
+ if ($set_default) {
+ for my $daemon (keys %$SDN_DAEMONS_DEFAULT) {
+ $daemon_status->{$daemon} = $SDN_DAEMONS_DEFAULT->{$daemon}
+ if !defined($daemon_status->{$daemon});
+ }
+ }
+
+ my $old_config = PVE::Tools::file_get_contents($daemons_file);
+ my $new_config = "";
+
+ my $changed = 0;
+
+ my @lines = split(/\n/, $old_config);
+
+ for my $line (@lines) {
+ if ($line =~ m/^([a-z_]+)=/) {
+ my $key = $1;
+ my $status = $daemon_status->{$key};
+
+ if (defined $status) {
+ my $value = $status ? "yes" : "no";
+ my $new_line = "$key=$value";
+
+ $changed = 1 if $new_line ne $line;
+
+ $line = $new_line;
+ }
+ }
+
+ $new_config .= "$line\n";
+ }
+
+ PVE::Tools::file_set_contents($daemons_file, $new_config);
+
+ return $changed;
+}
+
=head3 to_raw_config(\%frr_config)
Converts a given C<\%frr_config> to the raw config format.
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-network v3 11/21] sdn: commit fabrics config to running configuration
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (42 preceding siblings ...)
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 10/21] sdn: frr: add daemon status to frr helper Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 12/21] fabrics: generate ifupdown configuration Stefan Hanreich
` (30 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
Save the fabrics configuration in the running configuration, when
applying the SDN configuration. This causes the FRR configuration to
be actually generated for the openfabric and ospf plugins, since the
FRR configuration is generated from the running configuration.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Network/SDN.pm | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index 8c9ce8c..4b735b5 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -176,13 +176,15 @@ sub commit_config {
my $zones_cfg = PVE::Network::SDN::Zones::config();
my $controllers_cfg = PVE::Network::SDN::Controllers::config();
my $subnets_cfg = PVE::Network::SDN::Subnets::config();
+ my $fabrics_cfg = PVE::Network::SDN::Fabrics::config();
my $vnets = { ids => $vnets_cfg->{ids} };
my $zones = { ids => $zones_cfg->{ids} };
my $controllers = { ids => $controllers_cfg->{ids} };
my $subnets = { ids => $subnets_cfg->{ids} };
+ my $fabrics = { ids => $fabrics_cfg->to_sections() };
- $cfg = { version => $version, vnets => $vnets, zones => $zones, controllers => $controllers, subnets => $subnets };
+ $cfg = { version => $version, vnets => $vnets, zones => $zones, controllers => $controllers, subnets => $subnets, fabrics => $fabrics };
cfs_write_file($running_cfg, $cfg);
}
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-network v3 12/21] fabrics: generate ifupdown configuration
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (43 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 11/21] sdn: commit fabrics config to running configuration Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 13/21] fabrics: add jsonschema for fabrics and nodes Stefan Hanreich
` (29 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
Currently, the ifupdown config generation is handled solely by the
zones plugin. Since the fabrics need to generate ifupdown
configuration as well, we create a new helper in the SDN module. It
then in turn calls into the zone and fabrics plugin, and merges the
generated raw configuration before writing it to the
/etc/network/interfaces.d/sdn file.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Network/SDN.pm | 68 ++++++++++++++++++++++++++++------
src/PVE/Network/SDN/Fabrics.pm | 7 ++++
src/PVE/Network/SDN/Zones.pm | 10 -----
src/test/run_test_zones.pl | 2 +-
4 files changed, 64 insertions(+), 23 deletions(-)
diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index 4b735b5..79e19b1 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -237,18 +237,62 @@ sub get_local_vnets {
return $vnets;
}
-sub generate_zone_config {
- my $raw_config = PVE::Network::SDN::Zones::generate_etc_network_config();
- if ($raw_config) {
- eval {
- my $net_cfg = PVE::INotify::read_file('interfaces', 1);
- my $opts = $net_cfg->{data}->{options};
- log_warn("missing 'source /etc/network/interfaces.d/sdn' directive for SDN support!\n")
- if ! grep { $_->[1] =~ m!^source /etc/network/interfaces.d/(:?sdn|\*)! } @$opts;
- };
- log_warn("Failed to read network interfaces definition - $@") if $@;
- }
- PVE::Network::SDN::Zones::write_etc_network_config($raw_config);
+=head3 generate_raw_etc_network_config()
+
+Generate the /etc/network/interfaces.d/sdn config file from the Zones
+and Fabrics configuration and return it as a String.
+
+=cut
+
+sub generate_raw_etc_network_config {
+ my $raw_config = "";
+
+ my $zone_config = PVE::Network::SDN::Zones::generate_etc_network_config();
+ $raw_config .= $zone_config if $zone_config;
+
+ my $fabric_config = PVE::Network::SDN::Fabrics::generate_etc_network_config();
+ $raw_config .= $fabric_config if $fabric_config;
+
+ return $raw_config;
+}
+
+=head3 ⋅write_raw_etc_network_config($raw_config)
+
+Writes a network configuration as generated by C<generate_raw_etc_network_config>
+to /etc/network/interfaces.d/sdn.
+
+=cut
+
+sub write_raw_etc_network_config {
+ my ($raw_config) = @_;
+ my $local_network_sdn_file = "/etc/network/interfaces.d/sdn";
+
+ die "no network config supplied" if !defined $raw_config;
+
+ eval {
+ my $net_cfg = PVE::INotify::read_file('interfaces', 1);
+ my $opts = $net_cfg->{data}->{options};
+ log_warn("missing 'source /etc/network/interfaces.d/sdn' directive for SDN support!\n")
+ if ! grep { $_->[1] =~ m!^source /etc/network/interfaces.d/(:?sdn|\*)! } @$opts;
+ };
+
+ log_warn("Failed to read network interfaces definition - $@") if $@;
+
+ my $writefh = IO::File->new($local_network_sdn_file,">");
+ print $writefh $raw_config;
+ $writefh->close();
+}
+
+=head3 ⋅generate_etc_network_config()
+
+Generates the network configuration for all SDN plugins and writes it to the SDN
+interfaces files (/etc/network/interfaces.d/sdn).
+
+=cut
+
+sub generate_etc_network_config {
+ my $raw_config = PVE::Network::SDN::generate_raw_etc_network_config();
+ PVE::Network::SDN::write_raw_etc_network_config($raw_config);
}
=head3 generate_frr_raw_config(\%running_config, \%fabric_config)
diff --git a/src/PVE/Network/SDN/Fabrics.pm b/src/PVE/Network/SDN/Fabrics.pm
index 4950a88..3ac01cd 100644
--- a/src/PVE/Network/SDN/Fabrics.pm
+++ b/src/PVE/Network/SDN/Fabrics.pm
@@ -74,4 +74,11 @@ sub generate_frr_raw_config {
return \@raw_config;
}
+sub generate_etc_network_config {
+ my $nodename = PVE::INotify::nodename();
+ my $fabric_config = PVE::Network::SDN::Fabrics::config(1);
+
+ return $fabric_config->get_interfaces_etc_network_config($nodename);
+}
+
1;
diff --git a/src/PVE/Network/SDN/Zones.pm b/src/PVE/Network/SDN/Zones.pm
index c1c7745..131ca5e 100644
--- a/src/PVE/Network/SDN/Zones.pm
+++ b/src/PVE/Network/SDN/Zones.pm
@@ -168,16 +168,6 @@ sub generate_etc_network_config {
return $raw_network_config;
}
-sub write_etc_network_config {
- my ($rawconfig) = @_;
-
- return if !$rawconfig;
-
- my $writefh = IO::File->new($local_network_sdn_file,">");
- print $writefh $rawconfig;
- $writefh->close();
-}
-
sub read_etc_network_config_version {
my $versionstr = PVE::Tools::file_read_firstline($local_network_sdn_file);
diff --git a/src/test/run_test_zones.pl b/src/test/run_test_zones.pl
index 4137da6..794cbdb 100755
--- a/src/test/run_test_zones.pl
+++ b/src/test/run_test_zones.pl
@@ -129,7 +129,7 @@ foreach my $test (@tests) {
my $name = $test;
my $expected = read_file("./$test/expected_sdn_interfaces");
- my $result = eval { PVE::Network::SDN::Zones::generate_etc_network_config() };
+ my $result = eval { PVE::Network::SDN::generate_raw_etc_network_config() };
if (my $err = $@) {
diag("got unexpected error - $err");
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-network v3 13/21] fabrics: add jsonschema for fabrics and nodes
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (44 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 12/21] fabrics: generate ifupdown configuration Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 14/21] api: fabrics: add root-level module Stefan Hanreich
` (28 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
Provide a JSONSchema for the new two entity types, fabric and node.
While both are stored in the same configuration file, there are two
separate API submodules for fabrics and nodes, so we need to separate
the schema definitions as well.
The schemas are equivalent to the API types defined in Rust. In the
future it should be possible to generate the JSONSchema directly from
those types, but for now we have to duplicate the schema here.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Network/SDN/Fabrics.pm | 206 +++++++++++++++++++++++++++++++++
1 file changed, 206 insertions(+)
diff --git a/src/PVE/Network/SDN/Fabrics.pm b/src/PVE/Network/SDN/Fabrics.pm
index 3ac01cd..4d499ce 100644
--- a/src/PVE/Network/SDN/Fabrics.pm
+++ b/src/PVE/Network/SDN/Fabrics.pm
@@ -8,6 +8,35 @@ use PVE::JSONSchema qw(get_standard_option);
use PVE::INotify;
use PVE::RS::SDN::Fabrics;
+PVE::JSONSchema::register_format('pve-sdn-fabric-id', sub {
+ my ($id, $noerr) = @_;
+
+ if ($id !~ m/^[a-zA-Z0-9][a-zA-Z0-9-]{0,6}[a-zA-Z0-9]?$/i) {
+ return undef if $noerr;
+ die "Fabric ID '$id' contains illegal characters\n";
+ }
+
+ return $id;
+});
+
+PVE::JSONSchema::register_standard_option('pve-sdn-fabric-id', {
+ description => "Identifier for SDN fabrics",
+ type => 'string',
+ format => 'pve-sdn-fabric-id',
+});
+
+PVE::JSONSchema::register_standard_option('pve-sdn-fabric-node-id', {
+ description => "Identifier for nodes in an SDN fabric",
+ type => 'string',
+ format => 'pve-node',
+});
+
+PVE::JSONSchema::register_standard_option('pve-sdn-fabric-protocol', {
+ description => "Type of configuration entry in an SDN Fabric section config",
+ type => 'string',
+ enum => ['openfabric', 'ospf'],
+});
+
cfs_register_file(
'sdn/fabrics.cfg',
\&parse_fabrics_config,
@@ -81,4 +110,181 @@ sub generate_etc_network_config {
return $fabric_config->get_interfaces_etc_network_config($nodename);
}
+sub node_properties {
+ my ($update) = @_;
+
+ my $properties = {
+ fabric_id => get_standard_option('pve-sdn-fabric-id'),
+ node_id => get_standard_option('pve-sdn-fabric-node-id'),
+ protocol => get_standard_option('pve-sdn-fabric-protocol'),
+ digest => get_standard_option('pve-config-digest'),
+ ip => {
+ type => 'string',
+ format => 'ipv4',
+ description => 'IPv4 address for this node',
+ optional => 1,
+ },
+ ip6 => {
+ type => 'string',
+ format => 'ipv6',
+ description => 'IPv6 address for this node',
+ optional => 1,
+ },
+ interfaces => {
+ # coerce this value into an array before parsing (oneOf workaround)
+ type => 'array',
+ 'type-property' => 'protocol',
+ oneOf => [
+ {
+ type => 'array',
+ 'instance-types' => ['openfabric'],
+ items => {
+ type => 'string',
+ format => {
+ name => {
+ type => 'string',
+ format => 'pve-iface',
+ description => 'Name of the network interface',
+ },
+ hello_multiplier => {
+ type => 'integer',
+ description => 'The hello_multiplier property of the interface',
+ optional => 1,
+ minimum => 2,
+ maximum => 100,
+ },
+ ip => {
+ type => 'string',
+ format => 'CIDRv4',
+ description => 'IPv4 address for this node',
+ optional => 1,
+ },
+ ip6 => {
+ type => 'string',
+ format => 'CIDRv6',
+ description => 'IPv6 address for this node',
+ optional => 1,
+ },
+ },
+ },
+ description => 'OpenFabric network interface',
+ optional => 1,
+ },
+ {
+ type => 'array',
+ 'instance-types' => ['ospf'],
+ items => {
+ type => 'string',
+ format => {
+ name => {
+ type => 'string',
+ format => 'pve-iface',
+ description => 'Name of the network interface',
+ },
+ ip => {
+ type => 'string',
+ format => 'CIDRv4',
+ description => 'IPv4 address for this node',
+ optional => 1,
+ },
+ },
+ },
+ description => 'OSPF network interface',
+ optional => 1,
+ },
+ ]
+ }
+ };
+
+ if ($update) {
+ $properties->{delete} = {
+ type => 'array',
+ items => {
+ type => 'string',
+ enum => ['interfaces', 'ip', 'ip6'],
+ },
+ optional => 1,
+ }
+ }
+
+ return $properties;
+}
+
+sub fabric_properties {
+ my ($update) = @_;
+
+ my $properties = {
+ id => get_standard_option('pve-sdn-fabric-id'),
+ protocol => get_standard_option('pve-sdn-fabric-protocol'),
+ digest => get_standard_option('pve-config-digest'),
+ ip_prefix => {
+ type => 'string',
+ format => 'CIDR',
+ description => 'The IP prefix for Node IPs',
+ optional => 1,
+ },
+ ip6_prefix => {
+ type => 'string',
+ format => 'CIDR',
+ description => 'The IP prefix for Node IPs',
+ optional => 1,
+ },
+ hello_interval => {
+ type => 'number',
+ 'type-property' => 'protocol',
+ 'instance-types' => ['openfabric'],
+ description => 'The hello_interval property for Openfabric',
+ optional => 1,
+ minimum => 1,
+ maximum => 600,
+ },
+ csnp_interval => {
+ type => 'number',
+ 'type-property' => 'protocol',
+ 'instance-types' => ['openfabric'],
+ description => 'The csnp_interval property for Openfabric',
+ optional => 1,
+ minimum => 1,
+ maximum => 600,
+ },
+ area => {
+ type => 'string',
+ 'type-property' => 'protocol',
+ 'instance-types' => ['ospf'],
+ description => 'OSPF area. Either a IPv4 address or a 32-bit number. Gets validated in rust.',
+ optional => 1,
+ },
+ };
+
+ if ($update) {
+ $properties->{delete} = {
+ # coerce this value into an array before parsing (oneOf workaround)
+ type => 'array',
+ 'type-property' => 'protocol',
+ oneOf => [
+ {
+ type => 'array',
+ 'instance-types' => ['openfabric'],
+ items => {
+ type => 'string',
+ enum => ['hello_interval', 'csnp_interval'],
+ },
+ optional => 1,
+ },
+ {
+ type => 'array',
+ 'instance-types' => ['ospf'],
+ items => {
+ type => 'string',
+ enum => ['area'],
+ },
+ optional => 1,
+ },
+ ]
+ }
+ }
+
+ return $properties;
+}
+
1;
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-network v3 14/21] api: fabrics: add root-level module
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (45 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 13/21] fabrics: add jsonschema for fabrics and nodes Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 15/21] api: fabrics: add fabric submodule Stefan Hanreich
` (27 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
There is one endpoint (/all) at the top-level that fetches both types
of fabric entities (fabrics & nodes) and lists them separately. This
is used for the main view, in order to avoid having to do two API
calls. It works analogous to the existing root-level SDN API calls
with the running / pending parameters.
Also, since the interfaces key is used in the node sections, we need
to add it to the function encoding the values so they are compared and
returned from the API properly, when the pending parameter is set.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN.pm | 7 ++
src/PVE/API2/Network/SDN/Fabrics.pm | 165 ++++++++++++++++++++++++++++
src/PVE/API2/Network/SDN/Makefile | 3 +-
src/PVE/Network/SDN.pm | 2 +-
4 files changed, 175 insertions(+), 2 deletions(-)
create mode 100644 src/PVE/API2/Network/SDN/Fabrics.pm
diff --git a/src/PVE/API2/Network/SDN.pm b/src/PVE/API2/Network/SDN.pm
index 910f89a..877d46d 100644
--- a/src/PVE/API2/Network/SDN.pm
+++ b/src/PVE/API2/Network/SDN.pm
@@ -17,6 +17,7 @@ use PVE::API2::Network::SDN::Vnets;
use PVE::API2::Network::SDN::Zones;
use PVE::API2::Network::SDN::Ipams;
use PVE::API2::Network::SDN::Dns;
+use PVE::API2::Network::SDN::Fabrics;
use base qw(PVE::RESTHandler);
@@ -45,6 +46,11 @@ __PACKAGE__->register_method ({
path => 'dns',
});
+__PACKAGE__->register_method ({
+ subclass => "PVE::API2::Network::SDN::Fabrics",
+ path => 'fabrics',
+});
+
__PACKAGE__->register_method({
name => 'index',
path => '',
@@ -76,6 +82,7 @@ __PACKAGE__->register_method({
{ id => 'controllers' },
{ id => 'ipams' },
{ id => 'dns' },
+ { id => 'fabrics' },
];
return $res;
diff --git a/src/PVE/API2/Network/SDN/Fabrics.pm b/src/PVE/API2/Network/SDN/Fabrics.pm
new file mode 100644
index 0000000..9333700
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Fabrics.pm
@@ -0,0 +1,165 @@
+package PVE::API2::Network::SDN::Fabrics;
+
+use strict;
+use warnings;
+
+use PVE::Tools qw(extract_param);
+
+use PVE::Network::SDN;
+use PVE::Network::SDN::Fabrics;
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method ({
+ name => 'index',
+ path => '',
+ method => 'GET',
+ permissions => {
+ check => ['perm', '/sdn/fabrics', [ 'SDN.Audit' ]],
+ },
+ description => "SDN Fabrics Index",
+ parameters => {
+ properties => {},
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {
+ subdir => { type => 'string' },
+ },
+ },
+ links => [ { rel => 'child', href => "{subdir}" } ],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $res = [
+ { subdir => 'all' },
+ ];
+
+ return $res;
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'list_all',
+ path => 'all',
+ method => 'GET',
+ permissions => {
+ description => "Only list fabrics where you have 'SDN.Audit' or 'SDN.Allocate' permissions on\n" .
+ "'/sdn/fabrics/<fabric>', only list nodes where you have 'Sys.Audit' or 'Sys.Modify' on /nodes/<node_id>",
+ user => 'all'
+ },
+ description => "SDN Fabrics Index",
+ parameters => {
+ properties => {
+ running => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display running config.",
+ },
+ pending => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display pending config.",
+ },
+ },
+ },
+ returns => {
+ type => 'object',
+ properties => {
+ fabrics => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => PVE::Network::SDN::Fabrics::fabric_properties(0),
+ },
+ },
+ nodes => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => PVE::Network::SDN::Fabrics::node_properties(0),
+ },
+ }
+ }
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $pending = extract_param($param, 'pending');
+ my $running = extract_param($param, 'running');
+
+ my $digest;
+ my $fabrics;
+ my $nodes;
+
+ if ($pending) {
+ my $current_config = PVE::Network::SDN::Fabrics::config();
+ my $running_config = PVE::Network::SDN::Fabrics::config(1);
+
+ my ($running_fabrics, $running_nodes) = $running_config
+ ->list_all();
+
+ my ($current_fabrics, $current_nodes) = $current_config
+ ->list_all();
+
+ my $pending_fabrics = PVE::Network::SDN::pending_config(
+ { fabrics => { ids => $running_fabrics }},
+ { ids => $current_fabrics },
+ 'fabrics'
+ );
+
+ my $pending_nodes = PVE::Network::SDN::pending_config(
+ { nodes => { ids => $running_nodes }},
+ { ids => $current_nodes },
+ 'nodes'
+ );
+
+ $digest = $current_config->digest();
+ $fabrics = $pending_fabrics->{ids};
+ $nodes = $pending_nodes->{ids};
+ } elsif ($running) {
+ ($fabrics, $nodes) = PVE::Network::SDN::Fabrics::config(1)
+ ->list_all();
+ } else {
+ my $current_config = PVE::Network::SDN::Fabrics::config();
+
+ ($fabrics, $nodes) = $current_config->list_all();
+ $digest = $current_config->digest();
+ }
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+ my $fabric_privs = ['SDN.Audit', 'SDN.Allocate'];
+ my $node_privs = ['Sys.Audit', 'Sys.Modify'];
+
+ my @res_fabrics;
+ for my $id (keys %$fabrics) {
+ next if !$rpcenv->check_any($authuser, "/sdn/fabrics/$id", $fabric_privs, 1);
+
+ $fabrics->{$id}->{digest} = $digest if $digest;
+ push @res_fabrics, $fabrics->{$id};
+ }
+
+ my @res_nodes;
+ for my $node_id (keys %$nodes) {
+ my $node = $nodes->{$node_id};
+ my $fabric_id = $node->{fabric_id} // $node->{pending}->{fabric_id};
+
+ next if !$rpcenv->check_any($authuser, "/sdn/fabrics/$fabric_id", $fabric_privs, 1);
+ next if !$rpcenv->check_any($authuser, "/nodes/$node_id", $node_privs, 1);
+
+ $node->{digest} = $digest if $digest;
+
+ push @res_nodes, $node;
+ }
+
+ return {
+ fabrics => \@res_fabrics,
+ nodes => \@res_nodes,
+ };
+ }});
+
+1;
diff --git a/src/PVE/API2/Network/SDN/Makefile b/src/PVE/API2/Network/SDN/Makefile
index abd1bfa..08bec75 100644
--- a/src/PVE/API2/Network/SDN/Makefile
+++ b/src/PVE/API2/Network/SDN/Makefile
@@ -1,4 +1,4 @@
-SOURCES=Vnets.pm Zones.pm Controllers.pm Subnets.pm Ipams.pm Dns.pm Ips.pm
+SOURCES=Vnets.pm Zones.pm Controllers.pm Subnets.pm Ipams.pm Dns.pm Ips.pm Fabrics.pm
PERL5DIR=${DESTDIR}/usr/share/perl5
@@ -7,4 +7,5 @@ PERL5DIR=${DESTDIR}/usr/share/perl5
install:
for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/API2/Network/SDN/$$i; done
make -C Zones install
+ make -C Fabrics install
diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index 79e19b1..1a340a5 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -362,7 +362,7 @@ sub generate_dhcp_config {
sub encode_value {
my ($type, $key, $value) = @_;
- if ($key eq 'nodes' || $key eq 'exitnodes' || $key eq 'dhcp-range') {
+ if ($key eq 'nodes' || $key eq 'exitnodes' || $key eq 'dhcp-range' || $key eq 'interfaces') {
if (ref($value) eq 'HASH') {
return join(',', sort keys(%$value));
} elsif (ref($value) eq 'ARRAY') {
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-network v3 15/21] api: fabrics: add fabric submodule
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (46 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 14/21] api: fabrics: add root-level module Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 16/21] api: fabrics: add node submodule Stefan Hanreich
` (26 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
This API module provides CRUD functionality for fabrics. The list
endpoint works analogous to the existing SDN endpoints with their
pending / running parameters.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN/Fabrics.pm | 8 +
src/PVE/API2/Network/SDN/Fabrics/Fabric.pm | 229 +++++++++++++++++++++
src/PVE/API2/Network/SDN/Fabrics/Makefile | 8 +
3 files changed, 245 insertions(+)
create mode 100644 src/PVE/API2/Network/SDN/Fabrics/Fabric.pm
create mode 100644 src/PVE/API2/Network/SDN/Fabrics/Makefile
diff --git a/src/PVE/API2/Network/SDN/Fabrics.pm b/src/PVE/API2/Network/SDN/Fabrics.pm
index 9333700..4ec9b79 100644
--- a/src/PVE/API2/Network/SDN/Fabrics.pm
+++ b/src/PVE/API2/Network/SDN/Fabrics.pm
@@ -8,9 +8,16 @@ use PVE::Tools qw(extract_param);
use PVE::Network::SDN;
use PVE::Network::SDN::Fabrics;
+use PVE::API2::Network::SDN::Fabrics::Fabric;
+
use PVE::RESTHandler;
use base qw(PVE::RESTHandler);
+__PACKAGE__->register_method ({
+ subclass => "PVE::API2::Network::SDN::Fabrics::Fabric",
+ path => 'fabric',
+});
+
__PACKAGE__->register_method ({
name => 'index',
path => '',
@@ -36,6 +43,7 @@ __PACKAGE__->register_method ({
my ($param) = @_;
my $res = [
+ { subdir => 'fabric' },
{ subdir => 'all' },
];
diff --git a/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm b/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm
new file mode 100644
index 0000000..028b352
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm
@@ -0,0 +1,229 @@
+package PVE::API2::Network::SDN::Fabrics::Fabric;
+
+use strict;
+use warnings;
+
+use PVE::Network::SDN;
+use PVE::Network::SDN::Fabrics;
+
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::Tools qw(extract_param);
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method ({
+ name => 'index',
+ path => '',
+ method => 'GET',
+ permissions => {
+ description => "Only list entries where you have 'SDN.Audit' or 'SDN.Allocate' permissions on '/sdn/fabrics/<fabric>'",
+ user => 'all'
+ },
+ description => "SDN Fabrics Index",
+ parameters => {
+ properties => {
+ running => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display running config.",
+ },
+ pending => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display pending config.",
+ },
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => 'object',
+ properties => PVE::Network::SDN::Fabrics::fabric_properties(0),
+ },
+ links => [ { rel => 'child', href => "{id}" } ],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $pending = extract_param($param, 'pending');
+ my $running = extract_param($param, 'running');
+
+ my $digest;
+ my $fabrics;
+
+ if ($pending) {
+ my $current_config = PVE::Network::SDN::Fabrics::config();
+ my $running_config = PVE::Network::SDN::Fabrics::config(1);
+
+ my $pending_fabrics = PVE::Network::SDN::pending_config(
+ { fabrics => { ids => $running_config->list_fabrics() }},
+ { ids => $current_config->list_fabrics() },
+ 'fabrics'
+ );
+
+ $digest = $current_config->digest();
+ $fabrics = $pending_fabrics->{ids};
+ } elsif ($running) {
+ $fabrics = PVE::Network::SDN::Fabrics::config(1)
+ ->list_fabrics();
+ } else {
+ my $current_config = PVE::Network::SDN::Fabrics::config();
+
+ $digest = $current_config->{digest};
+ $fabrics = $current_config->list_fabrics();
+ }
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+ my $privs = ['SDN.Audit', 'SDN.Allocate'];
+
+ my @res;
+ for my $id (keys %$fabrics) {
+ next if !$rpcenv->check_any($authuser, "/sdn/fabrics/$id", $privs, 1);
+ $fabrics->{$id}->{digest} = $digest if $digest;
+ push @res, $fabrics->{$id};
+ }
+
+ return \@res;
+ }});
+
+__PACKAGE__->register_method({
+ name => 'get_fabric',
+ path => '{id}',
+ method => 'GET',
+ description => 'Update a fabric',
+ permissions => {
+ check => ['perm', '/sdn/fabrics/{id}', [ 'SDN.Audit', 'SDN.Allocate' ], any => 1],
+ },
+ parameters => {
+ properties => {
+ id => get_standard_option('pve-sdn-fabric-id'),
+ },
+ },
+ returns => {
+ type => 'object',
+ properties => PVE::Network::SDN::Fabrics::fabric_properties(0),
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $id = extract_param($param, 'id');
+
+ my $config = PVE::Network::SDN::Fabrics::config();
+
+ my $fabric = $config->get_fabric($id);
+ $fabric->{digest} = $config->digest();
+
+ return $fabric;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'add_fabric',
+ path => '',
+ method => 'POST',
+ description => 'Add a fabric',
+ protected => 1,
+ permissions => {
+ check => ['perm', '/sdn/fabrics', [ 'SDN.Allocate' ]],
+ },
+ parameters => {
+ properties => PVE::Network::SDN::Fabrics::fabric_properties(0),
+ },
+ returns => {
+ type => 'null',
+ },
+ code => sub {
+ my ($param) = @_;
+
+ PVE::Network::SDN::lock_sdn_config(sub {
+ my $config = PVE::Network::SDN::Fabrics::config();
+
+ my $digest = extract_param($param, 'digest');
+ PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+ $config->add_fabric($param);
+ PVE::Network::SDN::Fabrics::write_config($config);
+ }, "adding fabric failed");
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'update_fabric',
+ path => '{id}',
+ method => 'PUT',
+ description => 'Update a fabric',
+ protected => 1,
+ permissions => {
+ check => ['perm', '/sdn/fabrics/{id}', [ 'SDN.Allocate' ]],
+ },
+ parameters => {
+ properties => PVE::Network::SDN::Fabrics::fabric_properties(1),
+ },
+ returns => {
+ type => 'null',
+ },
+ code => sub {
+ my ($param) = @_;
+
+ PVE::Network::SDN::lock_sdn_config(sub {
+ my $id = extract_param($param, 'id');
+
+ my $config = PVE::Network::SDN::Fabrics::config();
+
+ my $digest = extract_param($param, 'digest');
+ PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+ $config->update_fabric($id, $param);
+ PVE::Network::SDN::Fabrics::write_config($config);
+ }, "updating fabric failed");
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'delete_fabric',
+ path => '{id}',
+ method => 'DELETE',
+ description => 'Add a fabric',
+ protected => 1,
+ permissions => {
+ check => ['perm', '/sdn/fabrics/{id}', [ 'SDN.Allocate' ]],
+ },
+ parameters => {
+ properties => {
+ id => get_standard_option('pve-sdn-fabric-id'),
+ },
+ },
+ returns => {
+ type => 'null',
+ },
+ code => sub {
+ my ($param) = @_;
+
+ PVE::Network::SDN::lock_sdn_config(sub {
+ my $id = extract_param($param, 'id');
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+
+ my $config = PVE::Network::SDN::Fabrics::config();
+
+ my $nodes = $config->list_nodes_fabric($id);
+
+ for my $node_id (keys %$nodes) {
+ if (!$rpcenv->check_any($authuser, "/nodes/$node_id", ['Sys.Modify'], 1)) {
+ die "permission check failed: missing 'Sys.Modify' on node $node_id";
+ }
+ }
+
+ my $digest = extract_param($param, 'digest');
+ PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+ $config->delete_fabric($id);
+ PVE::Network::SDN::Fabrics::write_config($config);
+ }, "deleting fabric failed");
+ },
+});
+
+1;
diff --git a/src/PVE/API2/Network/SDN/Fabrics/Makefile b/src/PVE/API2/Network/SDN/Fabrics/Makefile
new file mode 100644
index 0000000..bd644f7
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Fabrics/Makefile
@@ -0,0 +1,8 @@
+SOURCES=Fabric.pm
+
+
+PERL5DIR=${DESTDIR}/usr/share/perl5
+
+.PHONY: install
+install:
+ for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/API2/Network/SDN/Fabrics/$$i; done
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-network v3 16/21] api: fabrics: add node submodule
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (47 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 15/21] api: fabrics: add fabric submodule Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 17/21] api: fabrics: add fabricnode submodule Stefan Hanreich
` (25 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
The GET endpoint lists all nodes from all fabrics - for listing the
nodes of a specific fabric or editing nodes another submodule will be
introduced below the node submodule in a future commit.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN/Fabrics.pm | 7 ++
src/PVE/API2/Network/SDN/Fabrics/Makefile | 2 +-
src/PVE/API2/Network/SDN/Fabrics/Node.pm | 107 ++++++++++++++++++++++
3 files changed, 115 insertions(+), 1 deletion(-)
create mode 100644 src/PVE/API2/Network/SDN/Fabrics/Node.pm
diff --git a/src/PVE/API2/Network/SDN/Fabrics.pm b/src/PVE/API2/Network/SDN/Fabrics.pm
index 4ec9b79..dd50aab 100644
--- a/src/PVE/API2/Network/SDN/Fabrics.pm
+++ b/src/PVE/API2/Network/SDN/Fabrics.pm
@@ -9,6 +9,7 @@ use PVE::Network::SDN;
use PVE::Network::SDN::Fabrics;
use PVE::API2::Network::SDN::Fabrics::Fabric;
+use PVE::API2::Network::SDN::Fabrics::Node;
use PVE::RESTHandler;
use base qw(PVE::RESTHandler);
@@ -18,6 +19,11 @@ __PACKAGE__->register_method ({
path => 'fabric',
});
+__PACKAGE__->register_method ({
+ subclass => "PVE::API2::Network::SDN::Fabrics::Node",
+ path => 'node',
+});
+
__PACKAGE__->register_method ({
name => 'index',
path => '',
@@ -44,6 +50,7 @@ __PACKAGE__->register_method ({
my $res = [
{ subdir => 'fabric' },
+ { subdir => 'node' },
{ subdir => 'all' },
];
diff --git a/src/PVE/API2/Network/SDN/Fabrics/Makefile b/src/PVE/API2/Network/SDN/Fabrics/Makefile
index bd644f7..169ebbe 100644
--- a/src/PVE/API2/Network/SDN/Fabrics/Makefile
+++ b/src/PVE/API2/Network/SDN/Fabrics/Makefile
@@ -1,4 +1,4 @@
-SOURCES=Fabric.pm
+SOURCES=Fabric.pm Node.pm
PERL5DIR=${DESTDIR}/usr/share/perl5
diff --git a/src/PVE/API2/Network/SDN/Fabrics/Node.pm b/src/PVE/API2/Network/SDN/Fabrics/Node.pm
new file mode 100644
index 0000000..958d387
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Fabrics/Node.pm
@@ -0,0 +1,107 @@
+package PVE::API2::Network::SDN::Fabrics::Node;
+
+use strict;
+use warnings;
+
+use PVE::Tools qw(extract_param);
+
+use PVE::Network::SDN;
+use PVE::Network::SDN::Fabrics;
+
+use PVE::JSONSchema qw(get_standard_option);
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method ({
+ name => 'list_nodes',
+ path => '',
+ method => 'GET',
+ permissions => {
+ description => "Only list nodes where you have 'SDN.Audit' or 'SDN.Allocate' permissions on\n" .
+ "'/sdn/fabrics/<fabric>' and 'Sys.Audit' or 'Sys.Modify' on /nodes/<node_id>",
+ user => 'all'
+ },
+ description => "SDN Fabrics Index",
+ parameters => {
+ properties => {
+ running => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display running config.",
+ },
+ pending => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display pending config.",
+ },
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => PVE::Network::SDN::Fabrics::node_properties(0),
+ },
+ links => [ { rel => 'child', href => "{fabric_id}" } ],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $pending = extract_param($param, 'pending');
+ my $running = extract_param($param, 'running');
+
+ my $digest;
+ my $nodes;
+
+ if ($pending) {
+ my $current_config = PVE::Network::SDN::Fabrics::config();
+ my $running_config = PVE::Network::SDN::Fabrics::config(1);
+
+ my $running_nodes = $running_config
+ ->list_nodes();
+
+ my $current_nodes = $current_config
+ ->list_nodes();
+
+ my $pending_nodes = PVE::Network::SDN::pending_config(
+ { nodes => { ids => $running_nodes }},
+ { ids => $current_nodes },
+ 'nodes'
+ );
+
+ $digest = $current_config->digest();
+ $nodes = $pending_nodes->{ids};
+ } elsif ($running) {
+ $nodes = PVE::Network::SDN::Fabrics::config(1)
+ ->list_nodes();
+ } else {
+ my $current_config = PVE::Network::SDN::Fabrics::config();
+
+ $digest = $current_config->digest();
+ $nodes = $current_config->list_nodes();
+ }
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+ my $fabric_privs = ['SDN.Audit', 'SDN.Allocate'];
+ my $node_privs = ['Sys.Audit', 'Sys.Modify'];
+
+ my @res;
+
+ for my $node_id (keys %$nodes) {
+ my $node = $nodes->{$node_id};
+ my $fabric_id = $node->{fabric_id};
+
+ next if !$rpcenv->check_any($authuser, "/sdn/fabrics/$fabric_id", $fabric_privs, 1);
+ next if !$rpcenv->check_any($authuser, "/nodes/$node_id", $node_privs, 1);
+
+ $node->{digest} = $digest if $digest;
+
+ push @res, $node;
+ }
+
+ return \@res;
+ }});
+
+1;
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-network v3 17/21] api: fabrics: add fabricnode submodule
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (48 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 16/21] api: fabrics: add node submodule Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 18/21] controller: evpn: add fabrics integration Stefan Hanreich
` (24 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
Provides CRUD functionality for editing nodes inside a fabric, as well
as an endpoint for listing all the nodes. The URL structure is modeled
after the fact that a node can only be uniquely identified by its ID
as well as the ID of the fabric that contains the node.
Since fabrics can be used to edit the network configuration, we
require addtional Sys.Modify permissions on the node itself, since
that is the permission that is currently required by other endpoints
that allow modifiying the network configuration.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
.../API2/Network/SDN/Fabrics/FabricNode.pm | 242 ++++++++++++++++++
src/PVE/API2/Network/SDN/Fabrics/Makefile | 2 +-
src/PVE/API2/Network/SDN/Fabrics/Node.pm | 6 +
3 files changed, 249 insertions(+), 1 deletion(-)
create mode 100644 src/PVE/API2/Network/SDN/Fabrics/FabricNode.pm
diff --git a/src/PVE/API2/Network/SDN/Fabrics/FabricNode.pm b/src/PVE/API2/Network/SDN/Fabrics/FabricNode.pm
new file mode 100644
index 0000000..1374197
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Fabrics/FabricNode.pm
@@ -0,0 +1,242 @@
+package PVE::API2::Network::SDN::Fabrics::FabricNode;
+
+use strict;
+use warnings;
+
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::Tools qw(extract_param);
+
+use PVE::Network::SDN;
+use PVE::Network::SDN::Fabrics;
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method ({
+ name => 'list_nodes_fabric',
+ path => '',
+ method => 'GET',
+ permissions => {
+ description => "Only returns nodes where you have 'Sys.Audit' or 'Sys.Modify' permissions.",
+ check => ['perm', '/sdn/fabrics/{fabric_id}', [ 'SDN.Audit' ]],
+ },
+ description => "SDN Fabrics Index",
+ parameters => {
+ properties => {
+ running => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display running config.",
+ },
+ pending => {
+ type => 'boolean',
+ optional => 1,
+ description => "Display pending config.",
+ },
+ fabric_id => get_standard_option('pve-sdn-fabric-id'),
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => PVE::Network::SDN::Fabrics::node_properties(0),
+ },
+ links => [ { rel => 'child', href => "{node_id}" } ],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $fabric_id = extract_param($param, 'fabric_id');
+ my $pending = extract_param($param, 'pending');
+ my $running = extract_param($param, 'running');
+
+ my $digest;
+ my $nodes;
+
+ if ($pending) {
+ my $current_config = PVE::Network::SDN::Fabrics::config();
+ my $running_config = PVE::Network::SDN::Fabrics::config(1);
+
+ my $running_nodes = $running_config
+ ->list_nodes_fabric($fabric_id);
+
+ my $current_nodes = $current_config
+ ->list_nodes_fabric($fabric_id);
+
+ my $pending_nodes = PVE::Network::SDN::pending_config(
+ { nodes => { ids => $running_nodes }},
+ { ids => $current_nodes },
+ 'nodes'
+ );
+
+ $digest = $current_config->digest();
+ $nodes = $pending_nodes->{ids};
+ } elsif ($running) {
+ $nodes = PVE::Network::SDN::Fabrics::config(1)
+ ->list_nodes_fabric($fabric_id);
+ } else {
+ my $current_config = PVE::Network::SDN::Fabrics::config();
+
+ $digest = $current_config->digest();
+ $nodes = $current_config->list_nodes_fabric($fabric_id);
+ }
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+ my $node_privs = ['Sys.Audit', 'Sys.Modify'];
+
+ my @res;
+ for my $node_id (sort keys %$nodes) {
+ next if !$rpcenv->check_any($authuser, "/nodes/$node_id", $node_privs, 1);
+ $nodes->{$node_id}->{digest} = $digest if $digest;
+ push @res, $nodes->{$node_id};
+ }
+
+ return \@res;
+ }});
+
+__PACKAGE__->register_method({
+ name => 'get_node',
+ path => '{node_id}',
+ method => 'GET',
+ description => 'Get a node',
+ permissions => {
+ check => ['and',
+ ['perm', '/sdn/fabrics/{fabric_id}', [ 'SDN.Audit', 'SDN.Allocate' ], any => 1],
+ ['perm', '/nodes/{node_id}', [ 'Sys.Audit', 'Sys.Modify' ], any => 1],
+ ],
+ },
+ parameters => {
+ properties => {
+ fabric_id => get_standard_option('pve-sdn-fabric-id'),
+ node_id => get_standard_option('pve-sdn-fabric-node-id'),
+ }
+ },
+ returns => {
+ properties => PVE::Network::SDN::Fabrics::node_properties(0),
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $fabric_id = extract_param($param, 'fabric_id');
+ my $node_id = extract_param($param, 'node_id');
+
+ my $config = PVE::Network::SDN::Fabrics::config();
+
+ my $node = $config->get_node($fabric_id, $node_id);
+ $node->{digest} = $config->digest();
+
+ return $node;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'add_node',
+ path => '',
+ method => 'POST',
+ description => 'Add a node',
+ protected => 1,
+ permissions => {
+ check => ['and',
+ ['perm', '/sdn/fabrics/{fabric_id}', [ 'SDN.Allocate' ]],
+ ['perm', '/nodes/{node_id}', [ 'Sys.Modify' ]],
+ ],
+ },
+ parameters => {
+ properties => PVE::Network::SDN::Fabrics::node_properties(0),
+ },
+ returns => {
+ type => 'null',
+ },
+ code => sub {
+ my ($param) = @_;
+
+ PVE::Network::SDN::lock_sdn_config(sub {
+ my $config = PVE::Network::SDN::Fabrics::config();
+
+ my $digest = extract_param($param, 'digest');
+ PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+ $config->add_node($param);
+ PVE::Network::SDN::Fabrics::write_config($config);
+ }, "adding node failed");
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'update_node',
+ path => '{node_id}',
+ method => 'PUT',
+ description => 'Update a node',
+ protected => 1,
+ permissions => {
+ check => ['and',
+ ['perm', '/sdn/fabrics/{fabric_id}', [ 'SDN.Allocate' ]],
+ ['perm', '/nodes/{node_id}', [ 'Sys.Modify' ]],
+ ],
+ },
+ parameters => {
+ properties => PVE::Network::SDN::Fabrics::node_properties(1),
+ },
+ returns => {
+ type => 'null',
+ },
+ code => sub {
+ my ($param) = @_;
+
+ PVE::Network::SDN::lock_sdn_config(sub {
+ my $fabric_id = extract_param($param, 'fabric_id');
+ my $node_id = extract_param($param, 'node_id');
+
+ my $config = PVE::Network::SDN::Fabrics::config();
+
+ my $digest = extract_param($param, 'digest');
+ PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+ $config->update_node($fabric_id, $node_id, $param);
+ PVE::Network::SDN::Fabrics::write_config($config);
+ }, "updating node failed");
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'delete_node',
+ path => '{node_id}',
+ method => 'DELETE',
+ description => 'Add a node',
+ protected => 1,
+ permissions => {
+ check => ['and',
+ ['perm', '/sdn/fabrics/{fabric_id}', [ 'SDN.Allocate' ]],
+ ['perm', '/nodes/{node_id}', [ 'Sys.Modify' ]],
+ ],
+ },
+ parameters => {
+ properties => {
+ fabric_id => get_standard_option('pve-sdn-fabric-id'),
+ node_id => get_standard_option('pve-sdn-fabric-node-id'),
+ }
+ },
+ returns => {
+ type => 'null',
+ },
+ code => sub {
+ my ($param) = @_;
+
+ PVE::Network::SDN::lock_sdn_config(sub {
+ my $fabric_id = extract_param($param, 'fabric_id');
+ my $node_id = extract_param($param, 'node_id');
+
+ my $config = PVE::Network::SDN::Fabrics::config();
+
+ my $digest = extract_param($param, 'digest');
+ PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+ $config->delete_node($fabric_id, $node_id);
+ PVE::Network::SDN::Fabrics::write_config($config);
+ }, "deleting node failed");
+ },
+});
+
+1;
diff --git a/src/PVE/API2/Network/SDN/Fabrics/Makefile b/src/PVE/API2/Network/SDN/Fabrics/Makefile
index 169ebbe..9c4bda8 100644
--- a/src/PVE/API2/Network/SDN/Fabrics/Makefile
+++ b/src/PVE/API2/Network/SDN/Fabrics/Makefile
@@ -1,4 +1,4 @@
-SOURCES=Fabric.pm Node.pm
+SOURCES=Fabric.pm FabricNode.pm Node.pm
PERL5DIR=${DESTDIR}/usr/share/perl5
diff --git a/src/PVE/API2/Network/SDN/Fabrics/Node.pm b/src/PVE/API2/Network/SDN/Fabrics/Node.pm
index 958d387..fafaadb 100644
--- a/src/PVE/API2/Network/SDN/Fabrics/Node.pm
+++ b/src/PVE/API2/Network/SDN/Fabrics/Node.pm
@@ -7,12 +7,18 @@ use PVE::Tools qw(extract_param);
use PVE::Network::SDN;
use PVE::Network::SDN::Fabrics;
+use PVE::API2::Network::SDN::Fabrics::FabricNode;
use PVE::JSONSchema qw(get_standard_option);
use PVE::RESTHandler;
use base qw(PVE::RESTHandler);
+__PACKAGE__->register_method ({
+ subclass => "PVE::API2::Network::SDN::Fabrics::FabricNode",
+ path => '{fabric_id}',
+});
+
__PACKAGE__->register_method ({
name => 'list_nodes',
path => '',
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-network v3 18/21] controller: evpn: add fabrics integration
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (49 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 17/21] api: fabrics: add fabricnode submodule Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 19/21] zone: vxlan: " Stefan Hanreich
` (23 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
Provide a new option to the EVPN controller, fabric, that can be used
to define a fabric as the underlay network for the EVPN controller.
When applying the configuration, the EVPN controller then
automatically generates the peer list and from the fabric
configuration, rather than users having to specify all IP addresses
manually. This also means that the peer list automatically updates
when changing the fabric.
An EVPN controller can only either define a peer list or a fabric, but
not both. This requires the 'peers' property to now be optional, but
the existence of either fabric / peers is now validated in the
on_update_hook now instead.
MTU is set automatically to 1450 (because of VXLAN overhead) when
fabrics are used, unless otherwise specified in the EVPN zone
configuration, since there is currently now way of reliably accessing
the MTU of the interfaces of the fabric. This means users have to
manually specify the MTU for the EVPN controller when using fabrics.
This could be particularly relevant in the future, when Wireguard is
introduced as a fabric, which incurs an overhead of 80 bytes,
requiring users to manually set the MTU to 1370.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN/Fabrics/Fabric.pm | 10 ++
src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 143 +++++++++++++++---
src/PVE/Network/SDN/Zones/EvpnPlugin.pm | 56 +++++--
3 files changed, 177 insertions(+), 32 deletions(-)
diff --git a/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm b/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm
index 028b352..8b2e286 100644
--- a/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm
+++ b/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm
@@ -217,6 +217,16 @@ __PACKAGE__->register_method({
}
}
+ # check if this fabric is used in the evpn controller
+ my $controller_cfg = PVE::Network::SDN::Controllers::config();
+ for my $key (keys %{$controller_cfg->{ids}}) {
+ my $controller = $controller_cfg->{ids}->{$key};
+ if ($controller->{type} eq "evpn" &&
+ $controller->{fabric} eq $id) {
+ die "this fabric is still used in the EVPN controller \"$key\"";
+ }
+ }
+
my $digest = extract_param($param, 'digest');
PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
diff --git a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
index bde331f..8e00c94 100644
--- a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
@@ -10,6 +10,7 @@ use PVE::RESTEnvironment qw(log_warn);
use PVE::Network::SDN::Controllers::Plugin;
use PVE::Network::SDN::Zones::Plugin;
+use PVE::Network::SDN::Fabrics;
use Net::IP;
use base('PVE::Network::SDN::Controllers::Plugin');
@@ -26,6 +27,11 @@ sub properties {
minimum => 0,
maximum => 4294967296
},
+ fabric => {
+ description => "SDN fabric to use as underlay for this EVPN controller.",
+ type => 'string',
+ format => 'pve-sdn-fabric-id',
+ },
peers => {
description => "peers address list.",
type => 'string', format => 'ip-list'
@@ -36,7 +42,8 @@ sub properties {
sub options {
return {
'asn' => { optional => 0 },
- 'peers' => { optional => 0 },
+ 'peers' => { optional => 1 },
+ 'fabric' => { optional => 1 },
};
}
@@ -44,34 +51,76 @@ sub options {
sub generate_frr_config {
my ($class, $plugin_config, $controller_cfg, $id, $uplinks, $config) = @_;
- my @peers;
- @peers = PVE::Tools::split_list($plugin_config->{'peers'}) if $plugin_config->{'peers'};
-
my $local_node = PVE::INotify::nodename();
+ my @peers;
my $asn = $plugin_config->{asn};
my $ebgp = undef;
my $loopback = undef;
my $autortas = undef;
+ my $ifaceip = undef;
+ my $routerid = undef;
+
my $bgprouter = find_bgp_controller($local_node, $controller_cfg);
my $isisrouter = find_isis_controller($local_node, $controller_cfg);
+ if ($plugin_config->{'fabric'}) {
+ my $config = PVE::Network::SDN::Fabrics::config(1);
+
+ my $fabric = eval { $config->get_fabric($plugin_config->{fabric}) };
+ if ($@) {
+ log_warn("could not configure EVPN controller $plugin_config->{id}: $@");
+ return;
+ }
+
+ my $nodes = $config->list_nodes_fabric($plugin_config->{fabric});
+
+ my $current_node = eval { $config->get_node($plugin_config->{fabric}, $local_node) };
+ if ($@) {
+ log_warn("could not configure EVPN controller $plugin_config->{id}: $@");
+ return;
+ }
+
+ if (!$current_node->{ip}) {
+ log_warn("Node $local_node requires an IP in the fabric $fabric->{id} to configure the EVPN controller");
+ return;
+ }
+
+ for my $node_id (sort keys %$nodes) {
+ my $node = $nodes->{$node_id};
+ push @peers, $node->{ip} if $node->{ip};
+ }
+
+ $loopback = "dummy_$fabric->{id}";
+
+ $ifaceip = $current_node->{ip};
+ $routerid = $current_node->{ip};
+
+ } elsif ($plugin_config->{'peers'}) {
+ @peers = PVE::Tools::split_list($plugin_config->{'peers'});
+
+ if ($bgprouter) {
+ $loopback = $bgprouter->{loopback} if $bgprouter->{loopback};
+ } elsif ($isisrouter) {
+ $loopback = $isisrouter->{loopback} if $isisrouter->{loopback};
+ }
+
+ ($ifaceip, my $interface) = PVE::Network::SDN::Zones::Plugin::find_local_ip_interface_peers(\@peers, $loopback);
+ $routerid = PVE::Network::SDN::Controllers::Plugin::get_router_id($ifaceip, $interface);
+ } else {
+ log_warn("neither fabric nor peers configured for EVPN controller $plugin_config->{id}");
+ return;
+ }
+
if ($bgprouter) {
$ebgp = 1 if $plugin_config->{'asn'} ne $bgprouter->{asn};
- $loopback = $bgprouter->{loopback} if $bgprouter->{loopback};
$asn = $bgprouter->{asn} if $bgprouter->{asn};
$autortas = $plugin_config->{'asn'} if $ebgp;
- } elsif ($isisrouter) {
- $loopback = $isisrouter->{loopback} if $isisrouter->{loopback};
}
- return if !$asn;
-
+ return if !$asn || !$routerid;
my $bgp = $config->{frr}->{router}->{"bgp $asn"} //= {};
- my ($ifaceip, $interface) = PVE::Network::SDN::Zones::Plugin::find_local_ip_interface_peers(\@peers, $loopback);
- my $routerid = PVE::Network::SDN::Controllers::Plugin::get_router_id($ifaceip, $interface);
-
my $remoteas = $ebgp ? "external" : $asn;
#global options
@@ -134,28 +183,74 @@ sub generate_zone_frr_config {
$rt_import = [PVE::Tools::split_list($plugin_config->{'rt-import'})] if $plugin_config->{'rt-import'};
my $asn = $controller->{asn};
+
my @peers;
- @peers = PVE::Tools::split_list($controller->{'peers'}) if $controller->{'peers'};
my $ebgp = undef;
my $loopback = undef;
+ my $ifaceip = undef;
my $autortas = undef;
+ my $routerid = undef;
+
my $bgprouter = find_bgp_controller($local_node, $controller_cfg);
my $isisrouter = find_isis_controller($local_node, $controller_cfg);
- if($bgprouter) {
- $ebgp = 1 if $controller->{'asn'} ne $bgprouter->{asn};
- $loopback = $bgprouter->{loopback} if $bgprouter->{loopback};
+ if ($controller->{fabric}) {
+ my $config = PVE::Network::SDN::Fabrics::config(1);
+
+ my $fabric = eval { $config->get_fabric($controller->{fabric}) };
+ if ($@) {
+ log_warn("could not configure EVPN controller $controller->{id}: $@");
+ return;
+ }
+
+ my $nodes = $config->list_nodes_fabric($controller->{fabric});
+
+ my $current_node = eval { $config->get_node($controller->{fabric}, $local_node) };
+ if ($@) {
+ log_warn("could not configure EVPN controller $controller->{id}: $@");
+ return;
+ }
+
+ if (!$current_node->{ip}) {
+ log_warn("Node $local_node requires an IP in the fabric $fabric->{id} to configure the EVPN controller");
+ return;
+ }
+
+ for my $node (values %$nodes) {
+ push @peers, $node->{ip} if $node->{ip};
+ }
+
+ $loopback = "dummy_$fabric->{id}";
+
+ $ifaceip = $current_node->{ip};
+ $routerid = $current_node->{ip};
+
+ } elsif ($controller->{peers}) {
+ @peers = PVE::Tools::split_list($controller->{'peers'}) if $controller->{'peers'};
+
+
+ if($bgprouter) {
+ $loopback = $bgprouter->{loopback} if $bgprouter->{loopback};
+ } elsif ($isisrouter) {
+ $loopback = $isisrouter->{loopback} if $isisrouter->{loopback};
+ }
+
+ ($ifaceip, my $interface) = PVE::Network::SDN::Zones::Plugin::find_local_ip_interface_peers(\@peers, $loopback);
+ $routerid = PVE::Network::SDN::Controllers::Plugin::get_router_id($ifaceip, $interface);
+
+ } else {
+ log_warn("neither fabric nor peers configured for EVPN controller $controller->{id}");
+ return;
+ }
+
+ if ($bgprouter) {
+ $ebgp = 1 if $controller->{'asn'} ne $bgprouter->{asn};
$asn = $bgprouter->{asn} if $bgprouter->{asn};
$autortas = $controller->{'asn'} if $ebgp;
- } elsif ($isisrouter) {
- $loopback = $isisrouter->{loopback} if $isisrouter->{loopback};
}
return if !$vrf || !$vrfvxlan || !$asn;
- my ($ifaceip, $interface) = PVE::Network::SDN::Zones::Plugin::find_local_ip_interface_peers(\@peers, $loopback);
- my $routerid = PVE::Network::SDN::Controllers::Plugin::get_router_id($ifaceip, $interface);
-
my $is_gateway = $exitnodes->{$local_node};
# vrf
@@ -326,6 +421,12 @@ sub on_update_hook {
$controllernb++;
die "only 1 global evpn controller can be defined" if $controllernb >= 1;
}
+
+ my $controller = $controller_cfg->{ids}->{$controllerid};
+ if ($controller->{type} eq 'evpn') {
+ die "must have exactly one of peers / fabric defined"
+ if ($controller->{peers} && $controller->{fabric}) || !($controller->{peers} || $controller->{fabric});
+ }
}
sub find_bgp_controller {
diff --git a/src/PVE/Network/SDN/Zones/EvpnPlugin.pm b/src/PVE/Network/SDN/Zones/EvpnPlugin.pm
index 4843756..94cb582 100644
--- a/src/PVE/Network/SDN/Zones/EvpnPlugin.pm
+++ b/src/PVE/Network/SDN/Zones/EvpnPlugin.pm
@@ -119,24 +119,58 @@ sub generate_sdn_config {
die "missing vxlan tag" if !$tag;
die "missing controller" if !$controller;
- my @peers = PVE::Tools::split_list($controller->{'peers'});
-
+ my @peers;
my $loopback = undef;
- my $bgprouter = PVE::Network::SDN::Controllers::EvpnPlugin::find_bgp_controller($local_node, $controller_cfg);
- my $isisrouter = PVE::Network::SDN::Controllers::EvpnPlugin::find_isis_controller($local_node, $controller_cfg);
- if ($bgprouter->{loopback}) {
- $loopback = $bgprouter->{loopback};
- } elsif ($isisrouter->{loopback}) {
- $loopback = $isisrouter->{loopback};
+ my $ifaceip = undef;
+ my $iface = undef;
+ my $routerid = undef;
+
+ if ($controller->{peers}) {
+ @peers = PVE::Tools::split_list($controller->{'peers'});
+
+ my $bgprouter = PVE::Network::SDN::Controllers::EvpnPlugin::find_bgp_controller($local_node, $controller_cfg);
+ my $isisrouter = PVE::Network::SDN::Controllers::EvpnPlugin::find_isis_controller($local_node, $controller_cfg);
+
+ if ($bgprouter->{loopback}) {
+ $loopback = $bgprouter->{loopback};
+ } elsif ($isisrouter->{loopback}) {
+ $loopback = $isisrouter->{loopback};
+ }
+
+ ($ifaceip, $iface) = PVE::Network::SDN::Zones::Plugin::find_local_ip_interface_peers(\@peers, $loopback);
+ } elsif ($controller->{fabric}) {
+ my $config = PVE::Network::SDN::Fabrics::config(1);
+
+ my $fabric = eval { $config->get_fabric($controller->{fabric}) };
+ die "could not configure EVPN zone $plugin_config->{id}: $@" if $@;
+
+ my $nodes = $config->list_nodes_fabric($controller->{fabric});
+
+ my $current_node = eval { $config->get_node($controller->{fabric}, $local_node) };
+ die "could not configure EVPN zone $plugin_config->{id}: $@" if $@;
+
+ die "Node $local_node requires an IP in the fabric $fabric->{id} to configure the EVPN zone"
+ if !$current_node->{ip};
+
+ for my $node (values %$nodes) {
+ push @peers, $node->{ip} if $node->{ip};
+ }
+
+ $loopback = "dummy_$fabric->{id}";
+
+ $ifaceip = $current_node->{ip};
+ $routerid = $current_node->{ip};
+ } else {
+ die "neither fabric nor peers configured for EVPN controller $controller->{id}";
}
- my ($ifaceip, $iface) = PVE::Network::SDN::Zones::Plugin::find_local_ip_interface_peers(\@peers, $loopback);
my $is_evpn_gateway = $plugin_config->{'exitnodes'}->{$local_node};
my $exitnodes_local_routing = $plugin_config->{'exitnodes-local-routing'};
-
my $mtu = 1450;
- $mtu = $interfaces_config->{$iface}->{mtu} - 50 if $interfaces_config->{$iface}->{mtu};
+ if ($iface) {
+ $mtu = $interfaces_config->{$iface}->{mtu} - 50 if $interfaces_config->{$iface}->{mtu};
+ }
$mtu = $plugin_config->{mtu} if $plugin_config->{mtu};
#vxlan interface
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-network v3 19/21] zone: vxlan: add fabrics integration
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (50 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 18/21] controller: evpn: add fabrics integration Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 20/21] test: fabrics: add test cases for ospf and openfabric + evpn Stefan Hanreich
` (22 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
Add a new property to the VXLAN zone, that can contain the name of a
fabric. This automatically generates the peer-list from the fabric,
instead of having to manually write a comma-separated IP list. This
changes the peer field to optional from required. Either the peers or
the fabric field needs to be set, and this is now validated in the
update hook of the VXLAN zone.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/API2/Network/SDN/Fabrics/Fabric.pm | 9 ++++
src/PVE/Network/SDN/Zones/VxlanPlugin.pm | 58 ++++++++++++++++++++--
2 files changed, 62 insertions(+), 5 deletions(-)
diff --git a/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm b/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm
index 8b2e286..490ea47 100644
--- a/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm
+++ b/src/PVE/API2/Network/SDN/Fabrics/Fabric.pm
@@ -227,6 +227,15 @@ __PACKAGE__->register_method({
}
}
+ # check if this fabric is used in a vxlan zone
+ my $zone_cfg = PVE::Network::SDN::Zones::config();
+ for my $key (keys %{$zone_cfg->{ids}}) {
+ my $zone = $zone_cfg->{ids}->{$key};
+ if ($zone->{type} eq "vxlan" && $zone->{fabric} eq $id) {
+ die "this fabric is still used in the VXLAN zone \"$key\"";
+ }
+ }
+
my $digest = extract_param($param, 'digest');
PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
diff --git a/src/PVE/Network/SDN/Zones/VxlanPlugin.pm b/src/PVE/Network/SDN/Zones/VxlanPlugin.pm
index 9a77bb9..5b282cf 100644
--- a/src/PVE/Network/SDN/Zones/VxlanPlugin.pm
+++ b/src/PVE/Network/SDN/Zones/VxlanPlugin.pm
@@ -29,6 +29,11 @@ sub properties {
description => "peers address list.",
type => 'string', format => 'ip-list'
},
+ fabric => {
+ description => "SDN fabric to use as underlay for this VXLAN zone.",
+ type => 'string',
+ format => 'pve-sdn-fabric-id',
+ },
'vxlan-port' => {
description => "Vxlan tunnel udp port (default 4789).",
minimum => 1,
@@ -41,13 +46,14 @@ sub properties {
sub options {
return {
nodes => { optional => 1},
- peers => { optional => 0 },
+ peers => { optional => 1 },
'vxlan-port' => { optional => 1 },
mtu => { optional => 1 },
dns => { optional => 1 },
reversedns => { optional => 1 },
dnszone => { optional => 1 },
ipam => { optional => 1 },
+ fabric => { optional => 1 },
};
}
@@ -59,16 +65,45 @@ sub generate_sdn_config {
my $alias = $vnet->{alias};
my $multicastaddress = $plugin_config->{'multicast-address'};
my $vxlanport = $plugin_config->{'vxlan-port'};
- my @peers;
- @peers = PVE::Tools::split_list($plugin_config->{'peers'}) if $plugin_config->{'peers'};
my $vxlan_iface = "vxlan_$vnetid";
die "missing vxlan tag" if !$tag;
- my ($ifaceip, $iface) = PVE::Network::SDN::Zones::Plugin::find_local_ip_interface_peers(\@peers);
+ my @peers;
+ my $ifaceip;
+ my $iface;
+
+ if ($plugin_config->{peers}) {
+ @peers = PVE::Tools::split_list($plugin_config->{'peers'}) if $plugin_config->{'peers'};
+ ($ifaceip, $iface) = PVE::Network::SDN::Zones::Plugin::find_local_ip_interface_peers(\@peers);
+ } elsif ($plugin_config->{fabric}) {
+ my $local_node = PVE::INotify::nodename();
+ my $config = PVE::Network::SDN::Fabrics::config(1);
+
+ my $fabric = eval { $config->get_fabric($plugin_config->{fabric}) };
+ die "could not configure VXLAN zone $plugin_config->{id}: $@" if $@;
+
+ my $nodes = $config->list_nodes_fabric($plugin_config->{fabric});
+
+ my $current_node = eval { $config->get_node($plugin_config->{fabric}, $local_node) };
+ die "could not configure VXLAN zone $plugin_config->{id}: $@" if $@;
+
+ die "Node $local_node requires an IP in the fabric $fabric->{id} to configure the VXLAN zone $plugin_config->{id}"
+ if !$current_node->{ip};
+
+ for my $node (values %$nodes) {
+ push @peers, $node->{ip} if $node->{ip};
+ }
+
+ $ifaceip = $current_node->{ip};
+ } else {
+ die "neither peers nor fabric configured for VXLAN zone $plugin_config->{id}";
+ }
my $mtu = 1450;
- $mtu = $interfaces_config->{$iface}->{mtu} - 50 if $interfaces_config->{$iface}->{mtu};
+ if ($iface) {
+ $mtu = $interfaces_config->{$iface}->{mtu} - 50 if $interfaces_config->{$iface}->{mtu};
+ }
$mtu = $plugin_config->{mtu} if $plugin_config->{mtu};
#vxlan interface
@@ -101,6 +136,19 @@ sub generate_sdn_config {
return $config;
}
+sub on_update_hook {
+ my ($class, $zoneid, $zone_cfg, $controller_cfg) = @_;
+
+ my $zone = $zone_cfg->{ids}->{$zoneid};
+
+ if (($zone->{peers} && $zone->{fabric}) || !($zone->{peers} || $zone->{fabric})) {
+ raise_param_exc({
+ peers => "must have exactly one of peers / fabric defined",
+ fabric => "must have exactly one of peers / fabric defined",
+ });
+ }
+}
+
sub vnet_update_hook {
my ($class, $vnet_cfg, $vnetid, $zone_cfg) = @_;
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-network v3 20/21] test: fabrics: add test cases for ospf and openfabric + evpn
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (51 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 19/21] zone: vxlan: " Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 21/21] frr: bump frr config version to 10.2.2 Stefan Hanreich
` (21 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
Add two additional test cases for EVPN zones, which use fabrics as the
underlay network - one for OSPF, one for OpenFabric. Those test cases
utilize the newly introduced fabric option in the EVPN controller.
Existing configurations, that use peers, are already covered by other
test cases.
The test cases cover a full-mesh fabric setup as well as a simple
point-to-point setup to a route reflector / spine. Those tests require
proxmox-perl-rs to be installed, so that they can run properly, since
they call into the rust code to generate the interface and FRR
configuration.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
.../expected_controller_config | 74 +++++++++++++++++
.../openfabric_fabric/expected_sdn_interfaces | 56 +++++++++++++
.../zones/evpn/openfabric_fabric/interfaces | 6 ++
.../zones/evpn/openfabric_fabric/sdn_config | 79 +++++++++++++++++++
.../ospf_fabric/expected_controller_config | 68 ++++++++++++++++
.../evpn/ospf_fabric/expected_sdn_interfaces | 53 +++++++++++++
src/test/zones/evpn/ospf_fabric/interfaces | 6 ++
src/test/zones/evpn/ospf_fabric/sdn_config | 76 ++++++++++++++++++
8 files changed, 418 insertions(+)
create mode 100644 src/test/zones/evpn/openfabric_fabric/expected_controller_config
create mode 100644 src/test/zones/evpn/openfabric_fabric/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/openfabric_fabric/interfaces
create mode 100644 src/test/zones/evpn/openfabric_fabric/sdn_config
create mode 100644 src/test/zones/evpn/ospf_fabric/expected_controller_config
create mode 100644 src/test/zones/evpn/ospf_fabric/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/ospf_fabric/interfaces
create mode 100644 src/test/zones/evpn/ospf_fabric/sdn_config
diff --git a/src/test/zones/evpn/openfabric_fabric/expected_controller_config b/src/test/zones/evpn/openfabric_fabric/expected_controller_config
new file mode 100644
index 0000000..749713a
--- /dev/null
+++ b/src/test/zones/evpn/openfabric_fabric/expected_controller_config
@@ -0,0 +1,74 @@
+frr version 8.5.2
+frr defaults datacenter
+hostname localhost
+log syslog informational
+service integrated-vtysh-config
+!
+!
+vrf vrf_evpn
+ vni 100
+exit-vrf
+!
+router bgp 65000
+ bgp router-id 172.20.3.1
+ no bgp hard-administrative-reset
+ no bgp default ipv4-unicast
+ coalesce-time 1000
+ no bgp graceful-restart notification
+ neighbor VTEP peer-group
+ neighbor VTEP remote-as 65000
+ neighbor VTEP bfd
+ neighbor VTEP update-source dummy_test
+ neighbor 172.20.3.2 peer-group VTEP
+ neighbor 172.20.3.3 peer-group VTEP
+ !
+ address-family l2vpn evpn
+ neighbor VTEP activate
+ neighbor VTEP route-map MAP_VTEP_IN in
+ neighbor VTEP route-map MAP_VTEP_OUT out
+ advertise-all-vni
+ exit-address-family
+exit
+!
+router bgp 65000 vrf vrf_evpn
+ bgp router-id 172.20.3.1
+ no bgp hard-administrative-reset
+ no bgp graceful-restart notification
+exit
+!
+route-map MAP_VTEP_IN permit 1
+exit
+!
+route-map MAP_VTEP_OUT permit 1
+exit
+router openfabric test
+ net 49.0001.1720.2000.3001.00
+exit
+!
+interface dummy_test
+ ip router openfabric test
+ openfabric passive
+exit
+!
+interface ens20
+ ip router openfabric test
+ openfabric hello-interval 1
+exit
+!
+interface ens21
+ ip router openfabric test
+ openfabric hello-interval 1
+exit
+!
+access-list pve_openfabric_test_ips permit 172.20.3.0/24
+!
+route-map pve_openfabric permit 100
+ match ip address pve_openfabric_test_ips
+ set src 172.20.3.1
+exit
+!
+ip protocol openfabric route-map pve_openfabric
+!
+!
+line vty
+!
\ No newline at end of file
diff --git a/src/test/zones/evpn/openfabric_fabric/expected_sdn_interfaces b/src/test/zones/evpn/openfabric_fabric/expected_sdn_interfaces
new file mode 100644
index 0000000..3efd220
--- /dev/null
+++ b/src/test/zones/evpn/openfabric_fabric/expected_sdn_interfaces
@@ -0,0 +1,56 @@
+#version:1
+
+auto vnet0
+iface vnet0
+ address 10.123.123.1/24
+ hwaddress BC:24:11:3B:39:34
+ bridge_ports vxlan_vnet0
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ ip-forward on
+ arp-accept on
+ vrf vrf_evpn
+
+auto vrf_evpn
+iface vrf_evpn
+ vrf-table auto
+ post-up ip route add vrf vrf_evpn unreachable default metric 4278198272
+
+auto vrfbr_evpn
+iface vrfbr_evpn
+ bridge-ports vrfvx_evpn
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ vrf vrf_evpn
+
+auto vrfvx_evpn
+iface vrfvx_evpn
+ vxlan-id 100
+ vxlan-local-tunnelip 172.20.3.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
+
+auto vxlan_vnet0
+iface vxlan_vnet0
+ vxlan-id 123456
+ vxlan-local-tunnelip 172.20.3.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
+
+auto dummy_test
+iface dummy_test inet static
+ address 172.20.3.1/32
+ link-type dummy
+ ip-forward 1
+
+auto ens20
+iface ens20
+ ip-forward 1
+
+auto ens21
+iface ens21
+ ip-forward 1
diff --git a/src/test/zones/evpn/openfabric_fabric/interfaces b/src/test/zones/evpn/openfabric_fabric/interfaces
new file mode 100644
index 0000000..1b4384b
--- /dev/null
+++ b/src/test/zones/evpn/openfabric_fabric/interfaces
@@ -0,0 +1,6 @@
+auto vmbr0
+iface vmbr0 inet static
+ address 172.20.3.1/32
+ bridge-ports eth0
+ bridge-stp off
+ bridge-fd 0
diff --git a/src/test/zones/evpn/openfabric_fabric/sdn_config b/src/test/zones/evpn/openfabric_fabric/sdn_config
new file mode 100644
index 0000000..60c7405
--- /dev/null
+++ b/src/test/zones/evpn/openfabric_fabric/sdn_config
@@ -0,0 +1,79 @@
+{
+ 'zones' => {
+ 'ids' => {
+ 'evpn' => {
+ 'type' => 'evpn',
+ 'ipam' => 'pve',
+ 'mac' => 'BC:24:11:3B:39:34',
+ 'controller' => 'ctrl',
+ 'vrf-vxlan' => 100
+ }
+ }
+ },
+ 'vnets' => {
+ 'ids' => {
+ 'vnet0' => {
+ 'zone' => 'evpn',
+ 'type' => 'vnet',
+ 'tag' => 123456
+ }
+ }
+ },
+ 'version' => 1,
+ 'subnets' => {
+ 'ids' => {
+ 'evpn-10.123.123.0-24' => {
+ 'vnet' => 'vnet0',
+ 'type' => 'subnet',
+ 'gateway' => '10.123.123.1'
+ }
+ }
+ },
+ 'controllers' => {
+ 'ids' => {
+ 'ctrl' => {
+ 'fabric' => 'test',
+ 'asn' => 65000,
+ 'type' => 'evpn'
+ }
+ }
+ },
+ 'fabrics' => {
+ 'ids' => {
+ 'test' => {
+ 'type' => 'openfabric_fabric',
+ 'id' => 'test',
+ 'hello_interval' => 1,
+ 'ip_prefix' => '172.20.3.0/24',
+ },
+ 'test_localhost' => {
+ 'interfaces' => [
+ 'name=ens20',
+ 'name=ens21'
+ ],
+ 'id' => 'test_localhost',
+ 'type' => 'openfabric_node',
+ 'ip' => '172.20.3.1',
+ },
+ 'test_pathfinder' => {
+ 'id' => 'test_pathfinder',
+ 'interfaces' => [
+ 'name=ens20',
+ 'name=ens21'
+ ],
+ 'ip' => '172.20.3.2',
+ 'type' => 'openfabric_node',
+ },
+ 'test_raider' => {
+ 'ip' => '172.20.3.3',
+ 'type' => 'openfabric_node',
+ 'interfaces' => [
+ 'name=ens21',
+ 'name=ens20'
+ ],
+ 'id' => 'test_raider',
+ }
+ }
+ }
+ };
+
diff --git a/src/test/zones/evpn/ospf_fabric/expected_controller_config b/src/test/zones/evpn/ospf_fabric/expected_controller_config
new file mode 100644
index 0000000..c7b2358
--- /dev/null
+++ b/src/test/zones/evpn/ospf_fabric/expected_controller_config
@@ -0,0 +1,68 @@
+frr version 8.5.2
+frr defaults datacenter
+hostname localhost
+log syslog informational
+service integrated-vtysh-config
+!
+!
+vrf vrf_evpn
+ vni 100
+exit-vrf
+!
+router bgp 65000
+ bgp router-id 172.20.30.1
+ no bgp hard-administrative-reset
+ no bgp default ipv4-unicast
+ coalesce-time 1000
+ no bgp graceful-restart notification
+ neighbor VTEP peer-group
+ neighbor VTEP remote-as 65000
+ neighbor VTEP bfd
+ neighbor VTEP update-source dummy_test
+ neighbor 172.20.30.2 peer-group VTEP
+ neighbor 172.20.30.3 peer-group VTEP
+ !
+ address-family l2vpn evpn
+ neighbor VTEP activate
+ neighbor VTEP route-map MAP_VTEP_IN in
+ neighbor VTEP route-map MAP_VTEP_OUT out
+ advertise-all-vni
+ exit-address-family
+exit
+!
+router bgp 65000 vrf vrf_evpn
+ bgp router-id 172.20.30.1
+ no bgp hard-administrative-reset
+ no bgp graceful-restart notification
+exit
+!
+route-map MAP_VTEP_IN permit 1
+exit
+!
+route-map MAP_VTEP_OUT permit 1
+exit
+router ospf
+ ospf router-id 172.20.30.1
+exit
+!
+interface dummy_test
+ ip ospf area 0
+ ip ospf passive
+exit
+!
+interface ens19
+ ip ospf area 0
+exit
+!
+access-list pve_ospf_test_ips permit 172.20.30.0/24
+!
+route-map pve_ospf permit 100
+ match ip address pve_ospf_test_ips
+ set src 172.20.30.1
+exit
+!
+ip protocol ospf route-map pve_ospf
+!
+!
+line vty
+!
\ No newline at end of file
diff --git a/src/test/zones/evpn/ospf_fabric/expected_sdn_interfaces b/src/test/zones/evpn/ospf_fabric/expected_sdn_interfaces
new file mode 100644
index 0000000..2543304
--- /dev/null
+++ b/src/test/zones/evpn/ospf_fabric/expected_sdn_interfaces
@@ -0,0 +1,53 @@
+#version:1
+
+auto vnet0
+iface vnet0
+ address 10.123.123.1/24
+ hwaddress BC:24:11:3B:39:34
+ bridge_ports vxlan_vnet0
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ ip-forward on
+ arp-accept on
+ vrf vrf_evpn
+
+auto vrf_evpn
+iface vrf_evpn
+ vrf-table auto
+ post-up ip route add vrf vrf_evpn unreachable default metric 4278198272
+
+auto vrfbr_evpn
+iface vrfbr_evpn
+ bridge-ports vrfvx_evpn
+ bridge_stp off
+ bridge_fd 0
+ mtu 1450
+ vrf vrf_evpn
+
+auto vrfvx_evpn
+iface vrfvx_evpn
+ vxlan-id 100
+ vxlan-local-tunnelip 172.20.30.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
+
+auto vxlan_vnet0
+iface vxlan_vnet0
+ vxlan-id 123456
+ vxlan-local-tunnelip 172.20.30.1
+ bridge-learning off
+ bridge-arp-nd-suppress on
+ mtu 1450
+
+auto dummy_test
+iface dummy_test inet static
+ address 172.20.30.1/32
+ link-type dummy
+ ip-forward 1
+
+auto ens19
+iface ens19 inet static
+ address 172.16.3.10/31
+ ip-forward 1
diff --git a/src/test/zones/evpn/ospf_fabric/interfaces b/src/test/zones/evpn/ospf_fabric/interfaces
new file mode 100644
index 0000000..79ba9c1
--- /dev/null
+++ b/src/test/zones/evpn/ospf_fabric/interfaces
@@ -0,0 +1,6 @@
+auto vmbr0
+iface vmbr0 inet static
+ address 172.20.30.1/32
+ bridge-ports eth0
+ bridge-stp off
+ bridge-fd 0
diff --git a/src/test/zones/evpn/ospf_fabric/sdn_config b/src/test/zones/evpn/ospf_fabric/sdn_config
new file mode 100644
index 0000000..aa17786
--- /dev/null
+++ b/src/test/zones/evpn/ospf_fabric/sdn_config
@@ -0,0 +1,76 @@
+{
+ 'zones' => {
+ 'ids' => {
+ 'evpn' => {
+ 'type' => 'evpn',
+ 'ipam' => 'pve',
+ 'mac' => 'BC:24:11:3B:39:34',
+ 'controller' => 'ctrl',
+ 'vrf-vxlan' => 100
+ }
+ }
+ },
+ 'vnets' => {
+ 'ids' => {
+ 'vnet0' => {
+ 'zone' => 'evpn',
+ 'type' => 'vnet',
+ 'tag' => 123456
+ }
+ }
+ },
+ 'version' => 1,
+ 'subnets' => {
+ 'ids' => {
+ 'evpn-10.123.123.0-24' => {
+ 'vnet' => 'vnet0',
+ 'type' => 'subnet',
+ 'gateway' => '10.123.123.1'
+ }
+ }
+ },
+ 'controllers' => {
+ 'ids' => {
+ 'ctrl' => {
+ 'fabric' => 'test',
+ 'asn' => 65000,
+ 'type' => 'evpn'
+ }
+ }
+ },
+ 'fabrics' => {
+ 'ids' => {
+ 'test_pathfinder' => {
+ 'id' => 'test_pathfinder',
+ 'interfaces' => [
+ 'name=ens19,ip=172.16.3.20/31'
+ ],
+ 'ip' => '172.20.30.2',
+ 'type' => 'ospf_node'
+ },
+ 'test' => {
+ 'ip_prefix' => '172.20.30.0/24',
+ 'area' => '0',
+ 'type' => 'ospf_fabric',
+ 'id' => 'test',
+ },
+ 'test_localhost' => {
+ 'id' => 'test_localhost',
+ 'interfaces' => [
+ 'name=ens19,ip=172.16.3.10/31'
+ ],
+ 'ip' => '172.20.30.1',
+ 'type' => 'ospf_node'
+ },
+ 'test_raider' => {
+ 'type' => 'ospf_node',
+ 'ip' => '172.20.30.3',
+ 'id' => 'test_raider',
+ 'interfaces' => [
+ 'name=ens19,ip=172.16.3.30/31'
+ ]
+ }
+ }
+ }
+ };
+
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-network v3 21/21] frr: bump frr config version to 10.2.2
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (52 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 20/21] test: fabrics: add test cases for ospf and openfabric + evpn Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH proxmox-widget-toolkit v3 1/1] network selector: add type parameter Stefan Hanreich
` (20 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
With the package bumped to 10.2.2 we need to generate the
configuration with the matching version, otherwise frr-reload.py fails
to create a delta of the configuration because of the version
mismatch. Reloading still works, but there is an ugly warning in the
reload log, that might throw off users.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/PVE/Network/SDN/Frr.pm | 2 +-
.../zones/evpn/advertise_subnets/expected_controller_config | 2 +-
.../evpn/disable_arp_nd_suppression/expected_controller_config | 2 +-
src/test/zones/evpn/ebgp/expected_controller_config | 2 +-
src/test/zones/evpn/ebgp_loopback/expected_controller_config | 2 +-
src/test/zones/evpn/exitnode/expected_controller_config | 2 +-
.../evpn/exitnode_local_routing/expected_controller_config | 2 +-
src/test/zones/evpn/exitnode_primary/expected_controller_config | 2 +-
src/test/zones/evpn/exitnode_snat/expected_controller_config | 2 +-
.../zones/evpn/exitnodenullroute/expected_controller_config | 2 +-
src/test/zones/evpn/ipv4/expected_controller_config | 2 +-
src/test/zones/evpn/ipv4ipv6/expected_controller_config | 2 +-
.../zones/evpn/ipv4ipv6nogateway/expected_controller_config | 2 +-
src/test/zones/evpn/ipv6/expected_controller_config | 2 +-
src/test/zones/evpn/ipv6underlay/expected_controller_config | 2 +-
src/test/zones/evpn/isis/expected_controller_config | 2 +-
src/test/zones/evpn/isis_loopback/expected_controller_config | 2 +-
src/test/zones/evpn/isis_standalone/expected_controller_config | 2 +-
src/test/zones/evpn/multipath_relax/expected_controller_config | 2 +-
src/test/zones/evpn/multiplezones/expected_controller_config | 2 +-
.../zones/evpn/openfabric_fabric/expected_controller_config | 2 +-
src/test/zones/evpn/ospf_fabric/expected_controller_config | 2 +-
src/test/zones/evpn/rt_import/expected_controller_config | 2 +-
src/test/zones/evpn/vxlanport/expected_controller_config | 2 +-
24 files changed, 24 insertions(+), 24 deletions(-)
diff --git a/src/PVE/Network/SDN/Frr.pm b/src/PVE/Network/SDN/Frr.pm
index 4950bf5..f98a0e8 100644
--- a/src/PVE/Network/SDN/Frr.pm
+++ b/src/PVE/Network/SDN/Frr.pm
@@ -211,7 +211,7 @@ sub raw_config_to_string {
my $nodename = PVE::INotify::nodename();
my @final_config = (
- "frr version 8.5.2",
+ "frr version 10.2.2",
"frr defaults datacenter",
"hostname $nodename",
"log syslog informational",
diff --git a/src/test/zones/evpn/advertise_subnets/expected_controller_config b/src/test/zones/evpn/advertise_subnets/expected_controller_config
index 473a470..ff2f530 100644
--- a/src/test/zones/evpn/advertise_subnets/expected_controller_config
+++ b/src/test/zones/evpn/advertise_subnets/expected_controller_config
@@ -1,4 +1,4 @@
-frr version 8.5.2
+frr version 10.2.2
frr defaults datacenter
hostname localhost
log syslog informational
diff --git a/src/test/zones/evpn/disable_arp_nd_suppression/expected_controller_config b/src/test/zones/evpn/disable_arp_nd_suppression/expected_controller_config
index 9d8ec60..f4057fd 100644
--- a/src/test/zones/evpn/disable_arp_nd_suppression/expected_controller_config
+++ b/src/test/zones/evpn/disable_arp_nd_suppression/expected_controller_config
@@ -1,4 +1,4 @@
-frr version 8.5.2
+frr version 10.2.2
frr defaults datacenter
hostname localhost
log syslog informational
diff --git a/src/test/zones/evpn/ebgp/expected_controller_config b/src/test/zones/evpn/ebgp/expected_controller_config
index 8dfb6de..c4b1521 100644
--- a/src/test/zones/evpn/ebgp/expected_controller_config
+++ b/src/test/zones/evpn/ebgp/expected_controller_config
@@ -1,4 +1,4 @@
-frr version 8.5.2
+frr version 10.2.2
frr defaults datacenter
hostname localhost
log syslog informational
diff --git a/src/test/zones/evpn/ebgp_loopback/expected_controller_config b/src/test/zones/evpn/ebgp_loopback/expected_controller_config
index 82eef11..3e00954 100644
--- a/src/test/zones/evpn/ebgp_loopback/expected_controller_config
+++ b/src/test/zones/evpn/ebgp_loopback/expected_controller_config
@@ -1,4 +1,4 @@
-frr version 8.5.2
+frr version 10.2.2
frr defaults datacenter
hostname localhost
log syslog informational
diff --git a/src/test/zones/evpn/exitnode/expected_controller_config b/src/test/zones/evpn/exitnode/expected_controller_config
index 99e933a..2c0fa74 100644
--- a/src/test/zones/evpn/exitnode/expected_controller_config
+++ b/src/test/zones/evpn/exitnode/expected_controller_config
@@ -1,4 +1,4 @@
-frr version 8.5.2
+frr version 10.2.2
frr defaults datacenter
hostname localhost
log syslog informational
diff --git a/src/test/zones/evpn/exitnode_local_routing/expected_controller_config b/src/test/zones/evpn/exitnode_local_routing/expected_controller_config
index 2bc5572..4eaec10 100644
--- a/src/test/zones/evpn/exitnode_local_routing/expected_controller_config
+++ b/src/test/zones/evpn/exitnode_local_routing/expected_controller_config
@@ -1,4 +1,4 @@
-frr version 8.5.2
+frr version 10.2.2
frr defaults datacenter
hostname localhost
log syslog informational
diff --git a/src/test/zones/evpn/exitnode_primary/expected_controller_config b/src/test/zones/evpn/exitnode_primary/expected_controller_config
index 28c91a5..8542a7b 100644
--- a/src/test/zones/evpn/exitnode_primary/expected_controller_config
+++ b/src/test/zones/evpn/exitnode_primary/expected_controller_config
@@ -1,4 +1,4 @@
-frr version 8.5.2
+frr version 10.2.2
frr defaults datacenter
hostname localhost
log syslog informational
diff --git a/src/test/zones/evpn/exitnode_snat/expected_controller_config b/src/test/zones/evpn/exitnode_snat/expected_controller_config
index 99e933a..2c0fa74 100644
--- a/src/test/zones/evpn/exitnode_snat/expected_controller_config
+++ b/src/test/zones/evpn/exitnode_snat/expected_controller_config
@@ -1,4 +1,4 @@
-frr version 8.5.2
+frr version 10.2.2
frr defaults datacenter
hostname localhost
log syslog informational
diff --git a/src/test/zones/evpn/exitnodenullroute/expected_controller_config b/src/test/zones/evpn/exitnodenullroute/expected_controller_config
index fc8ae67..aaa9400 100644
--- a/src/test/zones/evpn/exitnodenullroute/expected_controller_config
+++ b/src/test/zones/evpn/exitnodenullroute/expected_controller_config
@@ -1,4 +1,4 @@
-frr version 8.5.2
+frr version 10.2.2
frr defaults datacenter
hostname localhost
log syslog informational
diff --git a/src/test/zones/evpn/ipv4/expected_controller_config b/src/test/zones/evpn/ipv4/expected_controller_config
index 9d8ec60..f4057fd 100644
--- a/src/test/zones/evpn/ipv4/expected_controller_config
+++ b/src/test/zones/evpn/ipv4/expected_controller_config
@@ -1,4 +1,4 @@
-frr version 8.5.2
+frr version 10.2.2
frr defaults datacenter
hostname localhost
log syslog informational
diff --git a/src/test/zones/evpn/ipv4ipv6/expected_controller_config b/src/test/zones/evpn/ipv4ipv6/expected_controller_config
index 9d8ec60..f4057fd 100644
--- a/src/test/zones/evpn/ipv4ipv6/expected_controller_config
+++ b/src/test/zones/evpn/ipv4ipv6/expected_controller_config
@@ -1,4 +1,4 @@
-frr version 8.5.2
+frr version 10.2.2
frr defaults datacenter
hostname localhost
log syslog informational
diff --git a/src/test/zones/evpn/ipv4ipv6nogateway/expected_controller_config b/src/test/zones/evpn/ipv4ipv6nogateway/expected_controller_config
index 9d8ec60..f4057fd 100644
--- a/src/test/zones/evpn/ipv4ipv6nogateway/expected_controller_config
+++ b/src/test/zones/evpn/ipv4ipv6nogateway/expected_controller_config
@@ -1,4 +1,4 @@
-frr version 8.5.2
+frr version 10.2.2
frr defaults datacenter
hostname localhost
log syslog informational
diff --git a/src/test/zones/evpn/ipv6/expected_controller_config b/src/test/zones/evpn/ipv6/expected_controller_config
index 9d8ec60..f4057fd 100644
--- a/src/test/zones/evpn/ipv6/expected_controller_config
+++ b/src/test/zones/evpn/ipv6/expected_controller_config
@@ -1,4 +1,4 @@
-frr version 8.5.2
+frr version 10.2.2
frr defaults datacenter
hostname localhost
log syslog informational
diff --git a/src/test/zones/evpn/ipv6underlay/expected_controller_config b/src/test/zones/evpn/ipv6underlay/expected_controller_config
index fffd415..8dfb720 100644
--- a/src/test/zones/evpn/ipv6underlay/expected_controller_config
+++ b/src/test/zones/evpn/ipv6underlay/expected_controller_config
@@ -1,4 +1,4 @@
-frr version 8.5.2
+frr version 10.2.2
frr defaults datacenter
hostname localhost
log syslog informational
diff --git a/src/test/zones/evpn/isis/expected_controller_config b/src/test/zones/evpn/isis/expected_controller_config
index 9ec8c01..4778e70 100644
--- a/src/test/zones/evpn/isis/expected_controller_config
+++ b/src/test/zones/evpn/isis/expected_controller_config
@@ -1,4 +1,4 @@
-frr version 8.5.2
+frr version 10.2.2
frr defaults datacenter
hostname localhost
log syslog informational
diff --git a/src/test/zones/evpn/isis_loopback/expected_controller_config b/src/test/zones/evpn/isis_loopback/expected_controller_config
index 5a7f5c9..795c9df 100644
--- a/src/test/zones/evpn/isis_loopback/expected_controller_config
+++ b/src/test/zones/evpn/isis_loopback/expected_controller_config
@@ -1,4 +1,4 @@
-frr version 8.5.2
+frr version 10.2.2
frr defaults datacenter
hostname localhost
log syslog informational
diff --git a/src/test/zones/evpn/isis_standalone/expected_controller_config b/src/test/zones/evpn/isis_standalone/expected_controller_config
index 5c9bf1a..b95746b 100644
--- a/src/test/zones/evpn/isis_standalone/expected_controller_config
+++ b/src/test/zones/evpn/isis_standalone/expected_controller_config
@@ -1,4 +1,4 @@
-frr version 8.5.2
+frr version 10.2.2
frr defaults datacenter
hostname localhost
log syslog informational
diff --git a/src/test/zones/evpn/multipath_relax/expected_controller_config b/src/test/zones/evpn/multipath_relax/expected_controller_config
index a87cdc4..d83b5d0 100644
--- a/src/test/zones/evpn/multipath_relax/expected_controller_config
+++ b/src/test/zones/evpn/multipath_relax/expected_controller_config
@@ -1,4 +1,4 @@
-frr version 8.5.2
+frr version 10.2.2
frr defaults datacenter
hostname localhost
log syslog informational
diff --git a/src/test/zones/evpn/multiplezones/expected_controller_config b/src/test/zones/evpn/multiplezones/expected_controller_config
index 37f663a..c95c3db 100644
--- a/src/test/zones/evpn/multiplezones/expected_controller_config
+++ b/src/test/zones/evpn/multiplezones/expected_controller_config
@@ -1,4 +1,4 @@
-frr version 8.5.2
+frr version 10.2.2
frr defaults datacenter
hostname localhost
log syslog informational
diff --git a/src/test/zones/evpn/openfabric_fabric/expected_controller_config b/src/test/zones/evpn/openfabric_fabric/expected_controller_config
index 749713a..1b0b420 100644
--- a/src/test/zones/evpn/openfabric_fabric/expected_controller_config
+++ b/src/test/zones/evpn/openfabric_fabric/expected_controller_config
@@ -1,4 +1,4 @@
-frr version 8.5.2
+frr version 10.2.2
frr defaults datacenter
hostname localhost
log syslog informational
diff --git a/src/test/zones/evpn/ospf_fabric/expected_controller_config b/src/test/zones/evpn/ospf_fabric/expected_controller_config
index c7b2358..8701b76 100644
--- a/src/test/zones/evpn/ospf_fabric/expected_controller_config
+++ b/src/test/zones/evpn/ospf_fabric/expected_controller_config
@@ -1,4 +1,4 @@
-frr version 8.5.2
+frr version 10.2.2
frr defaults datacenter
hostname localhost
log syslog informational
diff --git a/src/test/zones/evpn/rt_import/expected_controller_config b/src/test/zones/evpn/rt_import/expected_controller_config
index 5bdb148..6b19797 100644
--- a/src/test/zones/evpn/rt_import/expected_controller_config
+++ b/src/test/zones/evpn/rt_import/expected_controller_config
@@ -1,4 +1,4 @@
-frr version 8.5.2
+frr version 10.2.2
frr defaults datacenter
hostname localhost
log syslog informational
diff --git a/src/test/zones/evpn/vxlanport/expected_controller_config b/src/test/zones/evpn/vxlanport/expected_controller_config
index 9d8ec60..f4057fd 100644
--- a/src/test/zones/evpn/vxlanport/expected_controller_config
+++ b/src/test/zones/evpn/vxlanport/expected_controller_config
@@ -1,4 +1,4 @@
-frr version 8.5.2
+frr version 10.2.2
frr defaults datacenter
hostname localhost
log syslog informational
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH proxmox-widget-toolkit v3 1/1] network selector: add type parameter
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (53 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 21/21] frr: bump frr config version to 10.2.2 Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 01/18] api: use new sdn config generation functions Stefan Hanreich
` (19 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
The network endpoint of the PVE API allows selecting interfaces by
type, but the network selector widget currently does not support
passing the type to the API call.
This is required for the SDN fabrics, which introduced a new special
type to this endpoint that additionally selects all SDN interfaces.
This can then be used to make SDN fabrics show up in the Migration
Settings dialog or in the Ceph Installation Wizards.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/form/NetworkSelector.js | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/src/form/NetworkSelector.js b/src/form/NetworkSelector.js
index caa7b7e..8fa3d60 100644
--- a/src/form/NetworkSelector.js
+++ b/src/form/NetworkSelector.js
@@ -8,7 +8,10 @@ Ext.define('Proxmox.form.NetworkSelectorController', {
if (!view.nodename) {
throw "missing custom view config: nodename";
}
- view.getStore().getProxy().setUrl('/api2/json/nodes/'+ view.nodename + '/network');
+ view
+ .getStore()
+ .getProxy()
+ .setUrl(`/api2/json/nodes/${view.nodename}/network${view.getQueryString()}`);
},
});
@@ -33,6 +36,11 @@ Ext.define('Proxmox.form.NetworkSelector', {
controller: 'proxmoxNetworkSelectorController',
+ type: undefined,
+ getQueryString: function() {
+ return this.type ? `?type=${this.type}` : '';
+ },
+
nodename: 'localhost',
setNodename: function(nodename) {
this.nodename = nodename;
@@ -41,7 +49,9 @@ Ext.define('Proxmox.form.NetworkSelector', {
// because of manual local copy of data for ip4/6
this.getPicker().refresh();
if (networkSelectorStore && typeof networkSelectorStore.getProxy === 'function') {
- networkSelectorStore.getProxy().setUrl('/api2/json/nodes/'+ nodename + '/network');
+ networkSelectorStore
+ .getProxy()
+ .setUrl(`/api2/json/nodes/${nodename}/network${this.getQueryString()}`);
networkSelectorStore.load();
}
},
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-manager v3 01/18] api: use new sdn config generation functions
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (54 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH proxmox-widget-toolkit v3 1/1] network selector: add type parameter Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 02/18] ui: fabrics: add model definitions for fabrics Stefan Hanreich
` (18 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
With the introduction of fabrics, frr configuration generation and
etc/network/interfaces generation has been reworked and renamed for
better clarity, since now not only zones / controllers are responsible
for generating the ifupdown / FRR configuration. Switch this endpoint
over to use the new functions.
We also add a new skip_frr parameter that skips FRR config generation
if set. With the old FRR config generation logic, we never wrote an
empty FRR configuration if all controllers got deleted. This meant
that deleting all controllers still left the previous FRR
configuration on the nodes, never disabling BGP / IS-IS. The new logic
now writes an empty configuration if there is no controller / fabric
configured, fixing this behavior. This has a side effect for users
with an existing FRR configuration not managed by SDN, but utilizing
other SDN features (zones, vnets, ...). Their manual FRR configuration
would get overwritten when applying an SDN configuration. This is
particularly an issue with full-mesh Ceph setups, that were set up
according to our Wiki guide [1]. User with such a full-mesh setup
could get their FRR configuration overwritten when using unrelated SDN
features. Since this endpoint is called *after* committing the new SDN
configuration, but handles writing the FRR configuration, we need a
way to signal this endpoint to skip writing the FRR configuration from
the `PUT /cluster/sdn` endpoint, where we can check for this case.
[1] https://pve.proxmox.com/mediawiki/index.php?title=Full_Mesh_Network_for_Ceph_Server&oldid=12146
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
PVE/API2/Network.pm | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/PVE/API2/Network.pm b/PVE/API2/Network.pm
index 12ee6cca0..2ff729f7a 100644
--- a/PVE/API2/Network.pm
+++ b/PVE/API2/Network.pm
@@ -777,6 +777,11 @@ __PACKAGE__->register_method({
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
+ skip_frr => {
+ type => 'boolean',
+ description => 'Whether FRR config generation should get skipped or not.',
+ optional => 1,
+ },
},
},
returns => { type => 'string' },
@@ -791,6 +796,8 @@ __PACKAGE__->register_method({
my $current_config_file = "/etc/network/interfaces";
my $new_config_file = "/etc/network/interfaces.new";
+ my $skip_frr = extract_param($param, 'skip_frr');
+
assert_ifupdown2_installed();
my $worker = sub {
@@ -798,7 +805,7 @@ __PACKAGE__->register_method({
rename($new_config_file, $current_config_file) if -e $new_config_file;
if ($have_sdn) {
- PVE::Network::SDN::generate_zone_config();
+ PVE::Network::SDN::generate_etc_network_config();
PVE::Network::SDN::generate_dhcp_config();
}
@@ -810,8 +817,8 @@ __PACKAGE__->register_method({
};
PVE::Tools::run_command(['ifreload', '-a'], errfunc => $err);
- if ($have_sdn) {
- PVE::Network::SDN::generate_controller_config(1);
+ if ($have_sdn && !$skip_frr) {
+ PVE::Network::SDN::generate_frr_config(1);
}
};
return $rpcenv->fork_worker('srvreload', 'networking', $authuser, $worker);
@@ -868,3 +875,5 @@ __PACKAGE__->register_method({
return undef;
}});
+
+1;
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-manager v3 02/18] ui: fabrics: add model definitions for fabrics
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (55 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 01/18] api: use new sdn config generation functions Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 03/18] fabric: add common interface panel Stefan Hanreich
` (17 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
Add the three model definitions for SDN fabrics in a shared Common
module, so they can be accessed by all UI components for the SDN
fabrics.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/sdn/fabrics/Common.js | 36 ++++++++++++++++++++++++++++++
2 files changed, 37 insertions(+)
create mode 100644 www/manager6/sdn/fabrics/Common.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index fdf0e8165..efb016948 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -307,6 +307,7 @@ JSSRC= \
sdn/zones/SimpleEdit.js \
sdn/zones/VlanEdit.js \
sdn/zones/VxlanEdit.js \
+ sdn/fabrics/Common.js \
storage/ContentView.js \
storage/BackupView.js \
storage/Base.js \
diff --git a/www/manager6/sdn/fabrics/Common.js b/www/manager6/sdn/fabrics/Common.js
new file mode 100644
index 000000000..32e89a35b
--- /dev/null
+++ b/www/manager6/sdn/fabrics/Common.js
@@ -0,0 +1,36 @@
+Ext.define('Pve.sdn.Fabric', {
+ extend: 'Ext.data.Model',
+ idProperty: 'name',
+ fields: [
+ 'id',
+ 'protocol',
+ 'ip_prefix',
+ 'ip6_prefix',
+ ],
+});
+
+Ext.define('Pve.sdn.Node', {
+ extend: 'Ext.data.Model',
+ idProperty: 'name',
+ fields: [
+ 'fabric_id',
+ 'node_id',
+ 'protocol',
+ 'ip',
+ 'ip6',
+ 'area',
+ ],
+});
+
+Ext.define('Pve.sdn.Interface', {
+ extend: 'Ext.data.Model',
+ idProperty: 'name',
+ fields: [
+ 'name',
+ 'ip',
+ 'ip6',
+ 'hello_interval',
+ 'hello_multiplier',
+ 'csnp_interval',
+ ],
+});
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-manager v3 03/18] fabric: add common interface panel
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (56 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 02/18] ui: fabrics: add model definitions for fabrics Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 04/18] fabric: add OpenFabric interface properties Stefan Hanreich
` (16 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
Implements a shared interface selector panel for openfabric and ospf
fabrics. This GridPanel combines data from two sources: the node
network interfaces (/nodes/<node>/network) and the fabrics section
configuration, displaying a merged view of both sources.
It implements the following warning states:
- When an interface has an IP address configured in
/etc/network/interfaces, we display a warning and disable the input
field, prompting users to configure addresses only via the fabrics
interface
- When addresses exist in both /etc/network/interfaces and
/etc/network/interfaces.d/sdn, we show a warning without disabling
the field, allowing users to remove the SDN interface configuration
while preserving the underlying one
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/sdn/fabrics/InterfacePanel.js | 220 +++++++++++++++++++++
2 files changed, 221 insertions(+)
create mode 100644 www/manager6/sdn/fabrics/InterfacePanel.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index efb016948..469a1092e 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -308,6 +308,7 @@ JSSRC= \
sdn/zones/VlanEdit.js \
sdn/zones/VxlanEdit.js \
sdn/fabrics/Common.js \
+ sdn/fabrics/InterfacePanel.js \
storage/ContentView.js \
storage/BackupView.js \
storage/Base.js \
diff --git a/www/manager6/sdn/fabrics/InterfacePanel.js b/www/manager6/sdn/fabrics/InterfacePanel.js
new file mode 100644
index 000000000..0633b3673
--- /dev/null
+++ b/www/manager6/sdn/fabrics/InterfacePanel.js
@@ -0,0 +1,220 @@
+Ext.define('PVE.sdn.Fabric.InterfacePanel', {
+ extend: 'Ext.grid.Panel',
+ mixins: ['Ext.form.field.Field'],
+
+ xtype: 'pveSDNFabricsInterfacePanel',
+
+ nodeInterfaces: {},
+
+ selModel: {
+ mode: 'SIMPLE',
+ type: 'checkboxmodel',
+ },
+
+ commonColumns: [
+ {
+ text: gettext('Status'),
+ dataIndex: 'status',
+ width: 30,
+ renderer: function(value, metaData, record) {
+ let me = this;
+
+ let warning;
+ let nodeInterface = me.nodeInterfaces[record.data.name];
+
+ if (!nodeInterface) {
+ warning = gettext('Interface does not exist on node');
+ } else if ((nodeInterface.ip && record.data.ip) || (nodeInterface.ip6 && record.data.ip6)) {
+ warning = gettext('Interface already has an address configured in /etc/network/interfaces');
+ } else if (nodeInterface.ip || nodeInterface.ip6) {
+ warning = gettext('Configure the IP in the fabric, instead of /etc/network/interfaces');
+ }
+
+ if (warning) {
+ metaData.tdAttr = `data-qtip="${Ext.htmlEncode(Ext.htmlEncode(warning))}"`;
+ return `<i class="fa warning fa-warning"></i>`;
+ }
+
+ return '';
+ },
+
+ },
+ {
+ text: gettext('Name'),
+ dataIndex: 'name',
+ flex: 2,
+ },
+ {
+ text: gettext('Type'),
+ dataIndex: 'type',
+ flex: 1,
+ },
+ {
+ text: gettext('IP'),
+ xtype: 'widgetcolumn',
+ dataIndex: 'ip',
+ flex: 1,
+ widget: {
+ xtype: 'proxmoxtextfield',
+ isFormField: false,
+ bind: {
+ disabled: '{record.isDisabled}',
+ },
+ },
+ },
+ ],
+
+ additionalColumns: [],
+
+ controller: {
+ onValueChange: function(field, value) {
+ let me = this;
+
+ let record = field.getWidgetRecord();
+
+ if (!record) {
+ return;
+ }
+
+ let column = field.getWidgetColumn();
+
+ record.set(column.dataIndex, value);
+ record.commit();
+
+ me.getView().checkChange();
+ },
+
+ control: {
+ field: {
+ change: 'onValueChange',
+ },
+ },
+ },
+
+ listeners: {
+ selectionchange: function() {
+ this.checkChange();
+ },
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ Ext.apply(me, {
+ store: Ext.create("Ext.data.Store", {
+ model: "Pve.sdn.Interface",
+ sorters: {
+ property: 'name',
+ direction: 'ASC',
+ },
+ }),
+ columns: me.commonColumns.concat(me.additionalColumns),
+ });
+
+ me.callParent();
+
+ Proxmox.Utils.monStoreErrors(me, me.getStore(), true);
+ me.initField();
+ },
+
+ setNodeInterfaces: function(interfaces) {
+ let me = this;
+
+ let nodeInterfaces = {};
+ for (const iface of interfaces) {
+ nodeInterfaces[iface.name] = iface;
+ }
+
+ me.nodeInterfaces = nodeInterfaces;
+
+ // reset value when setting new available interfaces
+ me.setValue([]);
+ },
+
+ getValue: function() {
+ let me = this;
+
+ return me.getSelection()
+ .map((rec) => {
+ let data = {};
+
+ for (const [key, value] of Object.entries(rec.data)) {
+ if (value === '' || value === undefined || value === null) {
+ continue;
+ }
+
+ if (['type', 'isDisabled'].includes(key)) {
+ continue;
+ }
+
+ data[key] = value;
+ }
+
+ return PVE.Parser.printPropertyString(data);
+ });
+ },
+
+ setValue: function(value) {
+ let me = this;
+
+ let store = me.getStore();
+
+ let selection = me.getSelectionModel();
+ selection.deselectAll();
+
+ let data = structuredClone(me.nodeInterfaces);
+
+ for (const iface of Object.values(data)) {
+ iface.isDisabled = iface.ip || iface.ip6;
+ }
+
+ let selected = [];
+ let fabricInterfaces = structuredClone(value);
+
+ for (let iface of fabricInterfaces) {
+ iface = PVE.Parser.parsePropertyString(iface);
+
+ selected.push(iface.name);
+
+ // if the fabric configuration defines an interface that was
+ // previously disabled, re-enable the field to allow editing of the
+ // value set in the fabric - we show a warning as well if there is
+ // already an IP configured in /e/n/i
+ iface.isDisabled = false;
+
+ if (Object.prototype.hasOwnProperty.call(data, iface.name)) {
+ data[iface.name] = {
+ ...data[iface.name],
+ // fabric properties have precedence
+ ...iface,
+ };
+ } else {
+ data[iface.name] = iface;
+ }
+ }
+
+ store.setData(Object.values(data));
+
+ let selected_records = selected.map((name) => store.findRecord('name', name));
+ selection.select(selected_records);
+
+ me.resetOriginalValue();
+ },
+
+ getSubmitData: function() {
+ let me = this;
+
+ let name = me.getName();
+ let value = me.getValue();
+
+ if (value.length === 0 && !me.isCreate) {
+ return {
+ 'delete': name,
+ };
+ }
+
+ return {
+ [name]: value,
+ };
+ },
+});
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-manager v3 04/18] fabric: add OpenFabric interface properties
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (57 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 03/18] fabric: add common interface panel Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 05/18] fabric: add OSPF " Stefan Hanreich
` (15 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
This component extends the InterfacePanel and adds Openfabric specific
form fields. Hello Multiplier is hidden by default, but can be
activated in the column settings of the DataGrid.
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
www/manager6/Makefile | 1 +
.../sdn/fabrics/openfabric/InterfacePanel.js | 34 +++++++++++++++++++
2 files changed, 35 insertions(+)
create mode 100644 www/manager6/sdn/fabrics/openfabric/InterfacePanel.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 469a1092e..f109f4b05 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -309,6 +309,7 @@ JSSRC= \
sdn/zones/VxlanEdit.js \
sdn/fabrics/Common.js \
sdn/fabrics/InterfacePanel.js \
+ sdn/fabrics/openfabric/InterfacePanel.js \
storage/ContentView.js \
storage/BackupView.js \
storage/Base.js \
diff --git a/www/manager6/sdn/fabrics/openfabric/InterfacePanel.js b/www/manager6/sdn/fabrics/openfabric/InterfacePanel.js
new file mode 100644
index 000000000..3a5615ebb
--- /dev/null
+++ b/www/manager6/sdn/fabrics/openfabric/InterfacePanel.js
@@ -0,0 +1,34 @@
+Ext.define('PVE.sdn.Fabric.OpenFabric.InterfacePanel', {
+ extend: 'PVE.sdn.Fabric.InterfacePanel',
+
+ additionalColumns: [
+ {
+ text: gettext('IPv6'),
+ xtype: 'widgetcolumn',
+ dataIndex: 'ip6',
+ flex: 1,
+ widget: {
+ xtype: 'proxmoxtextfield',
+ isFormField: false,
+ bind: {
+ disabled: '{record.isDisabled}',
+ },
+ },
+ },
+ {
+ text: gettext('Hello Multiplier'),
+ xtype: 'widgetcolumn',
+ dataIndex: 'hello_multiplier',
+ flex: 1,
+ hidden: true,
+ widget: {
+ xtype: 'proxmoxintegerfield',
+ isFormField: false,
+ bind: {
+ disabled: '{record.isDisabled}',
+ },
+ },
+ },
+ ],
+});
+
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-manager v3 05/18] fabric: add OSPF interface properties
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (58 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 04/18] fabric: add OpenFabric interface properties Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 06/18] fabric: add generic node edit panel Stefan Hanreich
` (14 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
Define an OSPF-specific InterfacePanel for future use (currently there
are no protocol-specific properties for OSPF interfaces).
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/sdn/fabrics/ospf/InterfacePanel.js | 3 +++
2 files changed, 4 insertions(+)
create mode 100644 www/manager6/sdn/fabrics/ospf/InterfacePanel.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index f109f4b05..3e9ecc766 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -310,6 +310,7 @@ JSSRC= \
sdn/fabrics/Common.js \
sdn/fabrics/InterfacePanel.js \
sdn/fabrics/openfabric/InterfacePanel.js \
+ sdn/fabrics/ospf/InterfacePanel.js \
storage/ContentView.js \
storage/BackupView.js \
storage/Base.js \
diff --git a/www/manager6/sdn/fabrics/ospf/InterfacePanel.js b/www/manager6/sdn/fabrics/ospf/InterfacePanel.js
new file mode 100644
index 000000000..29f0502fa
--- /dev/null
+++ b/www/manager6/sdn/fabrics/ospf/InterfacePanel.js
@@ -0,0 +1,3 @@
+Ext.define('PVE.sdn.Fabric.Ospf.InterfacePanel', {
+ extend: 'PVE.sdn.Fabric.InterfacePanel',
+});
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-manager v3 06/18] fabric: add generic node edit panel
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (59 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 05/18] fabric: add OSPF " Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 07/18] fabric: add OpenFabric node edit Stefan Hanreich
` (13 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
This component is the base EditWindow for Nodes of all protocols.
It utilizes the existing network endpoint for getting information on
the interfaces of the nodes, as well as the existing pveNodeSelector
component for displaying a node dropdown. In the future we could
provide a single endpoint that accumulates that information
cluster-wide and returns it, eliminating the need for multiple API
calls.
If the node is configured but currently not in the quorate partition,
we show a read-only panel with the current configuration and a warning.
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/sdn/fabrics/NodeEdit.js | 224 +++++++++++++++++++++++++++
2 files changed, 225 insertions(+)
create mode 100644 www/manager6/sdn/fabrics/NodeEdit.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 3e9ecc766..79f82674b 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -309,6 +309,7 @@ JSSRC= \
sdn/zones/VxlanEdit.js \
sdn/fabrics/Common.js \
sdn/fabrics/InterfacePanel.js \
+ sdn/fabrics/NodeEdit.js \
sdn/fabrics/openfabric/InterfacePanel.js \
sdn/fabrics/ospf/InterfacePanel.js \
storage/ContentView.js \
diff --git a/www/manager6/sdn/fabrics/NodeEdit.js b/www/manager6/sdn/fabrics/NodeEdit.js
new file mode 100644
index 000000000..431287d5b
--- /dev/null
+++ b/www/manager6/sdn/fabrics/NodeEdit.js
@@ -0,0 +1,224 @@
+Ext.define('PVE.sdn.Fabric.Node.Edit', {
+ extend: 'Proxmox.window.Edit',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ width: 800,
+ subject: gettext('Node'),
+
+ isCreate: undefined,
+
+ fabricId: undefined,
+ nodeId: undefined,
+ protocol: undefined,
+
+ disallowedNodes: [],
+
+ baseUrl: "/cluster/sdn/fabrics/node",
+
+ items: [
+ {
+ xtype: 'textfield',
+ name: 'digest',
+ hidden: true,
+ allowBlank: true,
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('IPv4'),
+ labelWidth: 120,
+ name: 'ip',
+ allowBlank: true,
+ skipEmptyText: true,
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ },
+ ],
+
+ additionalItems: [],
+
+ addAnotherCallback: undefined,
+
+ initComponent: function() {
+ let me = this;
+
+ me.isCreate = me.nodeId === undefined;
+ me.autoLoad = !me.isCreate;
+ me.method = me.isCreate ? 'POST' : 'PUT';
+
+ if (!me.isCreate) {
+ me.url = `${me.baseUrl}/${me.fabricId}/${me.nodeId}`;
+ } else {
+ me.url = `${me.baseUrl}/${me.fabricId}`;
+ }
+
+ me.nodeSelector = me.getNodeSelector();
+ me.interfaceSelector = me.getInterfaceSelector();
+
+ me.items = [
+ me.nodeSelector,
+ ...me.items,
+ ...me.additionalItems,
+ me.interfaceSelector,
+ ];
+
+ me.callParent();
+
+ if (me.isCreate && me.addAnotherCallback) {
+ let addAnotherBtn = Ext.create('Ext.Button', {
+ text: gettext('Create another'),
+ disabled: !me.isCreate,
+ handler: function() {
+ me.apiCallDone = (success, _response, _options) => {
+ if (success) {
+ me.addAnotherCallback();
+ }
+ };
+
+ me.submit();
+ },
+ });
+
+ let form = me.formPanel.getForm();
+
+ let set_button_status = function() {
+ let valid = form.isValid();
+ let dirty = form.isDirty();
+ addAnotherBtn.setDisabled(!valid || !(dirty || me.isCreate));
+ };
+
+ form.on('dirtychange', set_button_status);
+ form.on('validitychange', set_button_status);
+
+ me.getDockedItems()[0].add(addAnotherBtn);
+ }
+ },
+
+ loadNode: async function() {
+ let me = this;
+
+ if (me.isCreate) {
+ return {};
+ }
+
+ let req = await Proxmox.Async.api2({
+ url: `/cluster/sdn/fabrics/node/${me.fabricId}/${me.nodeId}`,
+ method: 'GET',
+ });
+
+ return req.result.data;
+ },
+
+ loadNodeInterfaces: async function() {
+ let me = this;
+
+ let req = await Proxmox.Async.api2({
+ url: `/api2/extjs/nodes/${me.nodeId}/network`,
+ method: 'GET',
+ });
+
+ return req.result.data.map((iface) => ({
+ name: iface.iface,
+ type: iface.type,
+ ip: iface.cidr,
+ ipv6: iface.cidr6,
+ }));
+ },
+
+ load: function() {
+ let me = this;
+
+ me.setLoading("fetching node information");
+
+ Promise.all([
+ me.loadNode(me.fabricId, me.nodeId),
+ me.loadNodeInterfaces(me.nodeId),
+ ])
+ .catch(Proxmox.Utils.alertResponseFailure)
+ .then(([node, nodeInterfaces]) => {
+ me.interfaceSelector.setNodeInterfaces(nodeInterfaces);
+ me.setValues(node);
+ })
+ .finally(() => {
+ me.setLoading(false);
+ });
+ },
+
+ getNodeSelector: function() {
+ let me = this;
+
+ return Ext.create('PVE.form.NodeSelector', {
+ xtype: 'pveNodeSelector',
+ reference: 'nodeselector',
+ fieldLabel: gettext('Node'),
+ labelWidth: 120,
+ name: 'node_id',
+ allowBlank: false,
+ disabled: !me.isCreate,
+ disallowedNodes: me.disallowedNodes,
+ onlineValidator: me.isCreate,
+ autoSelect: me.isCreate,
+ listeners: {
+ change: function(f, value) {
+ if (me.isCreate) {
+ me.nodeId = value;
+ me.load();
+ }
+ },
+ },
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Node'),
+ dataIndex: 'node',
+ sortable: true,
+ hideable: false,
+ flex: 1,
+ },
+ ],
+ },
+ store: {
+ fields: ['node'],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes',
+ },
+ sorters: [
+ {
+ property: 'node',
+ direction: 'ASC',
+ },
+ ],
+ listeners: {
+ load: function(store) {
+ if (store.count() === 0) {
+ Ext.Msg.alert(
+ gettext('Add Node'),
+ gettext('All available nodes are already part of the fabric'),
+ () => me.destroy(),
+ );
+ }
+ },
+ },
+ },
+ });
+ },
+
+ getInterfacePanel: function(protocol) {
+ const INTERFACE_PANELS = {
+ openfabric: 'PVE.sdn.Fabric.OpenFabric.InterfacePanel',
+ ospf: 'PVE.sdn.Fabric.Ospf.InterfacePanel',
+ };
+
+ return INTERFACE_PANELS[protocol];
+ },
+
+ getInterfaceSelector: function() {
+ let me = this;
+
+ return Ext.create(me.getInterfacePanel(me.protocol), {
+ name: 'interfaces',
+ });
+ },
+});
+
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-manager v3 07/18] fabric: add OpenFabric node edit
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (60 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 06/18] fabric: add generic node edit panel Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 08/18] fabric: add OSPF " Stefan Hanreich
` (12 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
Extend the common NodeEdit panel with the Openfabric specific
properties. While IPv6 is a property that can be configured on all
nodes in the config, it is currently not supported for OSPF so we only
show it for Openfabric nodes.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
www/manager6/Makefile | 1 +
.../sdn/fabrics/openfabric/NodeEdit.js | 22 +++++++++++++++++++
2 files changed, 23 insertions(+)
create mode 100644 www/manager6/sdn/fabrics/openfabric/NodeEdit.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 79f82674b..69c6446c5 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -311,6 +311,7 @@ JSSRC= \
sdn/fabrics/InterfacePanel.js \
sdn/fabrics/NodeEdit.js \
sdn/fabrics/openfabric/InterfacePanel.js \
+ sdn/fabrics/openfabric/NodeEdit.js \
sdn/fabrics/ospf/InterfacePanel.js \
storage/ContentView.js \
storage/BackupView.js \
diff --git a/www/manager6/sdn/fabrics/openfabric/NodeEdit.js b/www/manager6/sdn/fabrics/openfabric/NodeEdit.js
new file mode 100644
index 000000000..fd6b3f177
--- /dev/null
+++ b/www/manager6/sdn/fabrics/openfabric/NodeEdit.js
@@ -0,0 +1,22 @@
+Ext.define('PVE.sdn.Fabric.OpenFabric.Node.Edit', {
+ extend: 'PVE.sdn.Fabric.Node.Edit',
+ protocol: 'openfabric',
+
+ extraRequestParams: {
+ protocol: 'openfabric',
+ },
+
+ additionalItems: [
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('IPv6'),
+ labelWidth: 120,
+ name: 'ip6',
+ allowBlank: true,
+ skipEmptyText: true,
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ },
+ ],
+});
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-manager v3 08/18] fabric: add OSPF node edit
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (61 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 07/18] fabric: add OpenFabric node edit Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 09/18] fabric: add generic fabric edit panel Stefan Hanreich
` (11 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
Extend the generic NodeEdit panel for OSPF. Currently there are no
node-specific properties for OSPF, so leave the additionalItems empty.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/sdn/fabrics/ospf/NodeEdit.js | 8 ++++++++
2 files changed, 9 insertions(+)
create mode 100644 www/manager6/sdn/fabrics/ospf/NodeEdit.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 69c6446c5..fd5c0cfc9 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -313,6 +313,7 @@ JSSRC= \
sdn/fabrics/openfabric/InterfacePanel.js \
sdn/fabrics/openfabric/NodeEdit.js \
sdn/fabrics/ospf/InterfacePanel.js \
+ sdn/fabrics/ospf/NodeEdit.js \
storage/ContentView.js \
storage/BackupView.js \
storage/Base.js \
diff --git a/www/manager6/sdn/fabrics/ospf/NodeEdit.js b/www/manager6/sdn/fabrics/ospf/NodeEdit.js
new file mode 100644
index 000000000..cc49312b3
--- /dev/null
+++ b/www/manager6/sdn/fabrics/ospf/NodeEdit.js
@@ -0,0 +1,8 @@
+Ext.define('PVE.sdn.Fabric.Ospf.Node.Edit', {
+ extend: 'PVE.sdn.Fabric.Node.Edit',
+ protocol: 'ospf',
+
+ extraRequestParams: {
+ protocol: 'ospf',
+ },
+});
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-manager v3 09/18] fabric: add generic fabric edit panel
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (62 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 08/18] fabric: add OSPF " Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 10/18] fabric: add OpenFabric " Stefan Hanreich
` (10 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
Add generic base component to add and edit Fabrics, which contains the
fields required for every protocol. The properties for every protocol
are stored in different components and each extend this one.
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/sdn/fabrics/FabricEdit.js | 57 ++++++++++++++++++++++++++
2 files changed, 58 insertions(+)
create mode 100644 www/manager6/sdn/fabrics/FabricEdit.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index fd5c0cfc9..6075f3289 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -310,6 +310,7 @@ JSSRC= \
sdn/fabrics/Common.js \
sdn/fabrics/InterfacePanel.js \
sdn/fabrics/NodeEdit.js \
+ sdn/fabrics/FabricEdit.js \
sdn/fabrics/openfabric/InterfacePanel.js \
sdn/fabrics/openfabric/NodeEdit.js \
sdn/fabrics/ospf/InterfacePanel.js \
diff --git a/www/manager6/sdn/fabrics/FabricEdit.js b/www/manager6/sdn/fabrics/FabricEdit.js
new file mode 100644
index 000000000..10f7aa2b2
--- /dev/null
+++ b/www/manager6/sdn/fabrics/FabricEdit.js
@@ -0,0 +1,57 @@
+Ext.define('PVE.sdn.Fabric.Fabric.Edit', {
+ extend: 'Proxmox.window.Edit',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ fabricId: undefined,
+ baseUrl: "/cluster/sdn/fabrics/fabric",
+
+ items: [
+ {
+ xtype: 'textfield',
+ name: 'digest',
+ hidden: true,
+ allowBlank: true,
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('Name'),
+ labelWidth: 120,
+ name: 'id',
+ cbind: {
+ disabled: '{!isCreate}',
+ },
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('IPv4 Prefix'),
+ labelWidth: 120,
+ name: 'ip_prefix',
+ allowBlank: true,
+ skipEmptyText: true,
+ cbind: {
+ disabled: '{!isCreate}',
+ deleteEmpty: '{!isCreate}',
+ },
+ },
+ ],
+
+ additionalItems: [],
+
+ initComponent: function() {
+ let me = this;
+
+ me.isCreate = me.fabricId === undefined;
+ me.autoLoad = !me.isCreate;
+ me.method = me.isCreate ? 'POST' : 'PUT';
+
+ if (!me.isCreate) {
+ me.url = `${me.baseUrl}/${me.fabricId}`;
+ } else {
+ me.url = me.baseUrl;
+ }
+
+ me.items.push(...me.additionalItems);
+
+ me.callParent();
+ },
+});
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-manager v3 10/18] fabric: add OpenFabric fabric edit panel
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (63 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 09/18] fabric: add generic fabric edit panel Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 11/18] fabric: add OSPF " Stefan Hanreich
` (9 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
Add a component that extends the common FabricEdit component and adds
the OpenFabric-specific items to it. Those are currently the Hello
Interval and CSNP interval, which can be configured globally for all
members of the fabric.
Since OSPF currently does not provide IPv6 support (yet), we also move
the IPv6 prefix to the Openfabric edit panel, to avoid showing the
IPv6 prefix input field in the OSPF fabric edit panel.
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
www/manager6/Makefile | 1 +
.../sdn/fabrics/openfabric/FabricEdit.js | 47 +++++++++++++++++++
2 files changed, 48 insertions(+)
create mode 100644 www/manager6/sdn/fabrics/openfabric/FabricEdit.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 6075f3289..487844dcd 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -313,6 +313,7 @@ JSSRC= \
sdn/fabrics/FabricEdit.js \
sdn/fabrics/openfabric/InterfacePanel.js \
sdn/fabrics/openfabric/NodeEdit.js \
+ sdn/fabrics/openfabric/FabricEdit.js \
sdn/fabrics/ospf/InterfacePanel.js \
sdn/fabrics/ospf/NodeEdit.js \
storage/ContentView.js \
diff --git a/www/manager6/sdn/fabrics/openfabric/FabricEdit.js b/www/manager6/sdn/fabrics/openfabric/FabricEdit.js
new file mode 100644
index 000000000..7688db900
--- /dev/null
+++ b/www/manager6/sdn/fabrics/openfabric/FabricEdit.js
@@ -0,0 +1,47 @@
+Ext.define('PVE.sdn.Fabric.OpenFabric.Fabric.Edit', {
+ extend: 'PVE.sdn.Fabric.Fabric.Edit',
+
+ subject: 'OpenFabric',
+ onlineHelp: 'pvesdn_openfabric_fabric',
+
+ extraRequestParams: {
+ protocol: 'openfabric',
+ },
+
+ additionalItems: [
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('IPv6 Prefix'),
+ labelWidth: 120,
+ name: 'ip6_prefix',
+ allowBlank: true,
+ skipEmptyText: true,
+ cbind: {
+ disabled: '{!isCreate}',
+ deleteEmpty: '{!isCreate}',
+ },
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ fieldLabel: gettext('Hello Interval'),
+ labelWidth: 120,
+ name: 'hello_interval',
+ allowBlank: true,
+ skipEmptyText: true,
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ fieldLabel: gettext('CSNP Interval'),
+ labelWidth: 120,
+ name: 'csnp_interval',
+ allowBlank: true,
+ skipEmptyText: true,
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ },
+ ],
+});
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-manager v3 11/18] fabric: add OSPF fabric edit panel
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (64 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 10/18] fabric: add OpenFabric " Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 12/18] fabrics: Add main FabricView Stefan Hanreich
` (8 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
Extends the common FabricEdit component and adds the OSPF-specific
items to it.
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/sdn/fabrics/ospf/FabricEdit.js | 20 ++++++++++++++++++++
2 files changed, 21 insertions(+)
create mode 100644 www/manager6/sdn/fabrics/ospf/FabricEdit.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 487844dcd..7541c2d0c 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -316,6 +316,7 @@ JSSRC= \
sdn/fabrics/openfabric/FabricEdit.js \
sdn/fabrics/ospf/InterfacePanel.js \
sdn/fabrics/ospf/NodeEdit.js \
+ sdn/fabrics/ospf/FabricEdit.js \
storage/ContentView.js \
storage/BackupView.js \
storage/Base.js \
diff --git a/www/manager6/sdn/fabrics/ospf/FabricEdit.js b/www/manager6/sdn/fabrics/ospf/FabricEdit.js
new file mode 100644
index 000000000..eddbc8435
--- /dev/null
+++ b/www/manager6/sdn/fabrics/ospf/FabricEdit.js
@@ -0,0 +1,20 @@
+Ext.define('PVE.sdn.Fabric.Ospf.Fabric.Edit', {
+ extend: 'PVE.sdn.Fabric.Fabric.Edit',
+
+ subject: 'OSPF',
+ onlineHelp: 'pvesdn_ospf_fabric',
+
+ extraRequestParams: {
+ protocol: 'ospf',
+ },
+
+ additionalItems: [
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Area'),
+ labelWidth: 120,
+ name: 'area',
+ allowBlank: false,
+ },
+ ],
+});
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-manager v3 12/18] fabrics: Add main FabricView
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (65 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 11/18] fabric: add OSPF " Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 13/18] utils: avoid line-break in pending changes message Stefan Hanreich
` (7 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
TreeView that shows all the fabrics and nodes in a hierarchical
structure. It also shows all the pending changes from the
running-config. From here all entities in the fabrics can be added /
edited and deleted, utilizing the previously created EditWindow
components for Fabrics / Nodes.
We decided against including all the interfaces (as children of nodes
in the tree view) because otherwise the indentation would be too much
and detailed information on the interfaces is rarely needed, so we
only show the names of the configured interfaces instead.
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/dc/Config.js | 8 +
www/manager6/sdn/FabricsView.js | 464 ++++++++++++++++++++++++++++++++
3 files changed, 473 insertions(+)
create mode 100644 www/manager6/sdn/FabricsView.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 7541c2d0c..bb5f98d3a 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -307,6 +307,7 @@ JSSRC= \
sdn/zones/SimpleEdit.js \
sdn/zones/VlanEdit.js \
sdn/zones/VxlanEdit.js \
+ sdn/FabricsView.js \
sdn/fabrics/Common.js \
sdn/fabrics/InterfacePanel.js \
sdn/fabrics/NodeEdit.js \
diff --git a/www/manager6/dc/Config.js b/www/manager6/dc/Config.js
index b79ba8dcc..3af9479c6 100644
--- a/www/manager6/dc/Config.js
+++ b/www/manager6/dc/Config.js
@@ -229,6 +229,14 @@ Ext.define('PVE.dc.Config', {
hidden: true,
iconCls: 'fa fa-shield',
itemId: 'sdnfirewall',
+ },
+ {
+ xtype: 'pveSDNFabricView',
+ groups: ['sdn'],
+ title: gettext('Fabrics'),
+ hidden: true,
+ iconCls: 'fa fa-road',
+ itemId: 'sdnfabrics',
});
}
diff --git a/www/manager6/sdn/FabricsView.js b/www/manager6/sdn/FabricsView.js
new file mode 100644
index 000000000..4dd74da85
--- /dev/null
+++ b/www/manager6/sdn/FabricsView.js
@@ -0,0 +1,464 @@
+Ext.define('PVE.sdn.Fabric.TreeModel', {
+ extend: 'Ext.data.TreeModel',
+ idProperty: 'tree_id',
+});
+
+Ext.define('PVE.sdn.Fabric.View', {
+ extend: 'Ext.tree.Panel',
+
+ xtype: 'pveSDNFabricView',
+
+ onlineHelp: 'pvesdn_config_fabrics',
+
+ columns: [
+ {
+ xtype: 'treecolumn',
+ text: gettext('Name'),
+ dataIndex: 'node_id',
+ width: 200,
+ renderer: function(value, metaData, rec) {
+ if (rec.data.type === 'fabric') {
+ return PVE.Utils.render_sdn_pending(rec, rec.data.id, 'id');
+ }
+
+ return PVE.Utils.render_sdn_pending(rec, value, 'node_id');
+ },
+ },
+ {
+ text: gettext('Protocol'),
+ dataIndex: 'protocol',
+ width: 100,
+ renderer: function(value, metaData, rec) {
+ if (rec.data.type === 'fabric') {
+ const PROTOCOL_DISPLAY_NAMES = {
+ 'openfabric': 'OpenFabric',
+ 'ospf': 'OSPF',
+ };
+ const displayValue = PROTOCOL_DISPLAY_NAMES[value];
+ if (rec.data.state === undefined || rec.data.state === null) {
+ return Ext.htmlEncode(displayValue);
+ }
+ if (rec.data.state === 'deleted') {
+ if (value === undefined) {
+ return ' ';
+ } else {
+ let encoded = Ext.htmlEncode(displayValue);
+ return `<span style="text-decoration: line-through;">${encoded}</span>`;
+ }
+ }
+ return Ext.htmlEncode(displayValue);
+ }
+
+ return "";
+ },
+ },
+ {
+ text: gettext('Ipv4'),
+ dataIndex: 'ip',
+ width: 150,
+ renderer: function(value, metaData, rec) {
+ if (rec.data.type === 'fabric') {
+ return PVE.Utils.render_sdn_pending(rec, rec.data.ip_prefix, 'ip_prefix');
+ }
+
+ return PVE.Utils.render_sdn_pending(rec, value, 'ip');
+ },
+ },
+ {
+ text: gettext('Ipv6'),
+ dataIndex: 'ip6',
+ width: 150,
+ renderer: function(value, metaData, rec) {
+ if (rec.data.type === 'fabric') {
+ return PVE.Utils.render_sdn_pending(rec, rec.data.ip6_prefix, 'ip6_prefix');
+ }
+
+ return PVE.Utils.render_sdn_pending(rec, value, 'ip6');
+ },
+ },
+ {
+ header: gettext('Interfaces'),
+ width: 200,
+ dataIndex: 'interface',
+ renderer: function(value, metaData, rec) {
+ const interfaces = rec.data.pending?.interfaces || rec.data.interfaces || [];
+
+ let names = interfaces.map((iface) => {
+ const properties = Proxmox.Utils.parsePropertyString(iface);
+ return properties.name;
+ });
+
+ names.sort();
+ const displayValue = Ext.htmlEncode(names.join(", "));
+ if (rec.data.state === 'deleted') {
+ return `<span style="text-decoration: line-through;">${displayValue}</span>`;
+ }
+ return displayValue;
+ },
+ },
+ {
+ text: gettext('Action'),
+ xtype: 'actioncolumn',
+ dataIndex: 'text',
+ width: 100,
+ items: [
+ {
+ handler: 'addActionTreeColumn',
+ getTip: (_v, _m, _rec) => gettext('Add Node'),
+ getClass: (_v, _m, { data }) => {
+ if (data.type === 'fabric') {
+ return 'fa fa-plus-circle';
+ }
+
+ return 'pmx-hidden';
+ },
+ isActionDisabled: (_v, _r, _c, _i, { data }) => data.type !== 'fabric',
+ },
+ {
+ tooltip: gettext('Edit'),
+ handler: 'editAction',
+ getClass: (_v, _m, { data }) => {
+ // the fabric type (openfabric, ospf, etc.) cannot be edited
+ if (data.type && data.state !== 'deleted') {
+ return 'fa fa-pencil fa-fw';
+ }
+
+ return 'pmx-hidden';
+ },
+ isActionDisabled: (_v, _r, _c, _i, { data }) => !data.type,
+ },
+ {
+ tooltip: gettext('Delete'),
+ handler: 'deleteAction',
+ getClass: (_v, _m, { data }) => {
+ // the fabric type (openfabric, ospf, etc.) cannot be deleted
+ if (data.type && data.state !== 'deleted') {
+ return 'fa critical fa-trash-o';
+ }
+
+ return 'pmx-hidden';
+ },
+ isActionDisabled: (_v, _r, _c, _i, { data }) => !data.type,
+ },
+ ],
+ },
+ {
+ header: gettext('State'),
+ width: 100,
+ dataIndex: 'state',
+ renderer: function(value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending_state(rec, value);
+ },
+ },
+ ],
+
+ store: {
+ sorters: ['tree_id'],
+ model: 'PVE.sdn.Fabric.TreeModel',
+ },
+
+ layout: 'fit',
+ rootVisible: false,
+ animate: false,
+
+ initComponent: function() {
+ let me = this;
+
+ let addNodeButton = new Proxmox.button.Button({
+ text: gettext('Add Node'),
+ handler: 'addActionTbar',
+ disabled: true,
+ });
+
+ let setAddNodeButtonStatus = function() {
+ let selection = me.view.getSelection();
+
+ if (selection.length === 0) {
+ return;
+ }
+
+ let enabled = selection[0].data.type === 'fabric';
+ addNodeButton.setDisabled(!enabled);
+ };
+
+ Ext.apply(me, {
+ tbar: [
+ {
+ text: gettext('Add Fabric'),
+ menu: [
+ {
+ text: 'OpenFabric',
+ handler: 'addOpenfabric',
+ },
+ {
+ text: 'OSPF',
+ handler: 'addOspf',
+ },
+ ],
+ },
+ addNodeButton,
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Reload'),
+ handler: function() {
+ const view = this.up('panel');
+ view.getController().reload(undefined);
+ },
+ },
+ ],
+ listeners: {
+ selectionchange: setAddNodeButtonStatus,
+ },
+ });
+
+ me.callParent();
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ reload: function(successCallback) {
+ let me = this;
+
+ Proxmox.Utils.API2Request({
+ url: `/cluster/sdn/fabrics/all?pending=1`,
+ method: 'GET',
+ success: function(response, opts) {
+ let fabrics = {};
+
+ for (const fabric of response.result.data.fabrics) {
+ let mergedFabric = {
+ expanded: true,
+ type: 'fabric',
+ iconCls: 'fa fa-road x-fa-treepanel',
+ children: [],
+ ...fabric,
+ ...fabric.pending,
+ };
+
+ mergedFabric.tree_id = mergedFabric.id;
+
+ fabrics[mergedFabric.id] = mergedFabric;
+ }
+
+ for (const node of response.result.data.nodes) {
+ let mergedNode = {
+ type: 'node',
+ iconCls: 'fa fa-desktop x-fa-treepanel',
+ leaf: true,
+ ...node,
+ ...node.pending,
+ };
+
+ mergedNode.tree_id = `${mergedNode.fabric_id}_${mergedNode.node_id}`;
+
+ fabrics[mergedNode.fabric_id].children.push(mergedNode);
+ }
+
+ me.getView().setRootNode({
+ name: '__root',
+ expanded: true,
+ children: Object.values(fabrics),
+ });
+
+ if (successCallback) {
+ successCallback();
+ }
+ },
+ });
+ },
+
+ getFabricEditPanel: function(protocol) {
+ const FABRIC_PANELS = {
+ openfabric: 'PVE.sdn.Fabric.OpenFabric.Fabric.Edit',
+ ospf: 'PVE.sdn.Fabric.Ospf.Fabric.Edit',
+ };
+
+ return FABRIC_PANELS[protocol];
+ },
+
+ getNodeEditPanel: function(protocol) {
+ const NODE_PANELS = {
+ openfabric: 'PVE.sdn.Fabric.OpenFabric.Node.Edit',
+ ospf: 'PVE.sdn.Fabric.Ospf.Node.Edit',
+ };
+
+ return NODE_PANELS[protocol];
+ },
+
+ addOpenfabric: function() {
+ let me = this;
+ me.openFabricAddWindow('openfabric');
+ },
+
+ addOspf: function() {
+ let me = this;
+ me.openFabricAddWindow('ospf');
+ },
+
+ openFabricAddWindow: function(protocol) {
+ let me = this;
+
+ let component = me.getFabricEditPanel(protocol);
+
+ let window = Ext.create(component, {
+ autoShow: true,
+ autoLoad: false,
+ isCreate: true,
+ });
+
+ window.on('destroy', () => me.reload());
+ },
+
+ addActionTreeColumn: function(_grid, _rI, _cI, _item, _e, rec) {
+ this.openNodeAddWindow(rec.data);
+ },
+
+ addActionTbar: function() {
+ let me = this;
+
+ let selection = me.view.getSelection();
+
+ if (selection.length === 0) {
+ return;
+ }
+
+ if (selection[0].data.type === 'fabric') {
+ me.openNodeAddWindow(selection[0].data);
+ }
+ },
+
+ openNodeAddWindow: function(fabric) {
+ let me = this;
+
+ let component = me.getNodeEditPanel(fabric.protocol);
+
+ let disallowedNodes = fabric.children
+ .filter((node) => !node.state || node.state !== 'deleted')
+ .map((node) => node.node_id);
+
+ Ext.create(component, {
+ autoShow: true,
+ fabricId: fabric.id,
+ protocol: fabric.protocol,
+ disallowedNodes,
+ addAnotherCallback: () => {
+ let successCallback = () => {
+ let new_fabric = me.getView()
+ .getStore()
+ .findRecord('tree_id', fabric.tree_id);
+
+ me.openNodeAddWindow(new_fabric.data);
+ };
+
+ me.reload(successCallback);
+ },
+ apiCallDone: (success, _response, _options) => {
+ if (success) {
+ me.reload();
+ }
+ },
+ });
+ },
+
+ openFabricEditWindow: function(fabric) {
+ let me = this;
+
+ let component = me.getFabricEditPanel(fabric.protocol);
+
+ let window = Ext.create(component, {
+ autoShow: true,
+ fabricId: fabric.id,
+ });
+
+ window.on('destroy', () => me.reload());
+ },
+
+ openNodeEditWindow: function(node) {
+ let me = this;
+
+ let component = me.getNodeEditPanel(node.protocol);
+
+ let window = Ext.create(component, {
+ autoShow: true,
+ fabricId: node.fabric_id,
+ nodeId: node.node_id,
+ protocol: node.protocol,
+ });
+
+ window.on('destroy', () => me.reload());
+ },
+
+ editAction: function(_grid, _rI, _cI, _item, _e, rec) {
+ let me = this;
+
+ if (rec.data.type === 'fabric') {
+ me.openFabricEditWindow(rec.data);
+ } else if (rec.data.type === 'node') {
+ me.openNodeEditWindow(rec.data);
+ } else {
+ console.warn(`unknown type ${rec.data.type}`);
+ }
+ },
+
+ handleDeleteAction: function(url, message) {
+ let me = this;
+ let view = me.getView();
+
+ Ext.Msg.show({
+ title: gettext('Confirm'),
+ icon: Ext.Msg.WARNING,
+ message: Ext.htmlEncode(message),
+ buttons: Ext.Msg.YESNO,
+ defaultFocus: 'no',
+ callback: function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+
+ Proxmox.Utils.API2Request({
+ url,
+ method: 'DELETE',
+ waitMsgTarget: view,
+ failure: function(response, opts) {
+ Ext.Msg.alert(Proxmox.Utils.errorText, response.htmlStatus);
+ },
+ callback: () => me.reload(),
+ });
+ },
+ });
+ },
+
+ deleteAction: function(table, rI, cI, item, e, rec) {
+ let me = this;
+
+ if (rec.data.type === "fabric") {
+ let message = Ext.String.format(
+ gettext('Are you sure you want to remove the fabric "{0}"?'),
+ rec.data.id,
+ );
+
+ let url = `/cluster/sdn/fabrics/fabric/${rec.data.id}`;
+
+ me.handleDeleteAction(url, message);
+ } else if (rec.data.type === "node") {
+ let message = Ext.String.format(
+ gettext('Are you sure you want to remove the node "{0}" from the fabric "{1}"?'),
+ rec.data.node_id,
+ rec.data.fabric_id,
+ );
+
+ let url = `/cluster/sdn/fabrics/node/${rec.data.fabric_id}/${rec.data.node_id}`;
+
+ me.handleDeleteAction(url, message);
+ } else {
+ console.warn(`unknown type: ${rec.data.type}`);
+ }
+ },
+
+ init: function(view) {
+ let me = this;
+ me.reload();
+ },
+ },
+});
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-manager v3 13/18] utils: avoid line-break in pending changes message
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (66 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 12/18] fabrics: Add main FabricView Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 14/18] ui: permissions: add ACL path for fabrics Stefan Hanreich
` (6 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
Remove line-break when showing the current status of SDN configuration
objects. Otherwise the column would contain an additional newline,
making the row too large.
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
www/manager6/Utils.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 1f6778cdb..e5b0a3f3e 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -192,7 +192,7 @@ Ext.define('PVE.Utils', {
if (value === undefined) {
return ' ';
} else {
- return `<div style="text-decoration: line-through;">${Ext.htmlEncode(value)}</div>`;
+ return `<span style="text-decoration: line-through;">${Ext.htmlEncode(value)}</span>`;
}
} else if (rec.data.pending[key] !== undefined && rec.data.pending[key] !== null) {
if (rec.data.pending[key] === 'deleted') {
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-manager v3 14/18] ui: permissions: add ACL path for fabrics
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (67 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 13/18] utils: avoid line-break in pending changes message Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 15/18] api: network: add include_sdn / fabric type Stefan Hanreich
` (5 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
Expose the newly created ACL path for fabrics in the UI, so users can
configure them.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
www/manager6/data/PermPathStore.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/www/manager6/data/PermPathStore.js b/www/manager6/data/PermPathStore.js
index 72da2e9da..b2dd16b29 100644
--- a/www/manager6/data/PermPathStore.js
+++ b/www/manager6/data/PermPathStore.js
@@ -15,6 +15,7 @@ Ext.define('PVE.data.PermPathStore', {
{ 'value': '/mapping/usb' },
{ 'value': '/nodes' },
{ 'value': '/pool' },
+ { 'value': '/sdn/fabrics' },
{ 'value': '/sdn/zones' },
{ 'value': '/storage' },
{ 'value': '/vms' },
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-manager v3 15/18] api: network: add include_sdn / fabric type
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (68 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 14/18] ui: permissions: add ACL path for fabrics Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 16/18] ui: add sdn networks to ceph / migration Stefan Hanreich
` (4 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
In order to be able to show SDN networks in the network selector
dropdowns, we introduce a new type ('include_sdn') to the API endpoint
that lists network interfaces of a node. The return value for existing
parameters stays unchanged to preserve backwards-compatibility.
Callers have to explicitly pass the new type if they want SDN networks
included in the response as well. Only fabrics for which the current
user has any SDN permission (Audit/Use/Modify) are listed.
There is also a new type that only lists fabrics ('fabric'), which
works analogous to the current type filters.
There was a separate type for vnets as well, that is not used anywhere
but was defunct due to a missing check in the endpoint. This has now
been fixed and supplying vnet as the type should now only return
vnets.
This commit is preparation for integrating the fabrics with several
parts in the UI, such as the Ceph installation wizard and the
migration settings, which use the pveNetworkSelector component that
uses this endpoint to query available network interfaces.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
PVE/API2/Network.pm | 41 ++++++++++++++++++++++++++++++++---------
1 file changed, 32 insertions(+), 9 deletions(-)
diff --git a/PVE/API2/Network.pm b/PVE/API2/Network.pm
index 2ff729f7a..944c1a068 100644
--- a/PVE/API2/Network.pm
+++ b/PVE/API2/Network.pm
@@ -37,7 +37,7 @@ my $bond_mode_enum = [
'lacp-balance-tcp', # OVS
];
-my $network_type_enum = ['bridge', 'bond', 'eth', 'alias', 'vlan',
+my $network_type_enum = ['bridge', 'bond', 'eth', 'alias', 'vlan', 'fabric',
'OVSBridge', 'OVSBond', 'OVSPort', 'OVSIntPort', 'vnet'];
my $confdesc = {
@@ -214,7 +214,7 @@ __PACKAGE__->register_method({
type => {
description => "Only list specific interface types.",
type => 'string',
- enum => [ @$network_type_enum, 'any_bridge', 'any_local_bridge' ],
+ enum => [ @$network_type_enum, 'any_bridge', 'any_local_bridge', 'include_sdn' ],
optional => 1,
},
},
@@ -363,22 +363,45 @@ __PACKAGE__->register_method({
if (my $tfilter = $param->{type}) {
my $vnets;
+ my $fabrics;
- if ($have_sdn && $tfilter eq 'any_bridge') {
+ if ($have_sdn && $tfilter =~ /^(any_bridge|include_sdn|vnet)$/) {
$vnets = PVE::Network::SDN::get_local_vnets(); # returns already access-filtered
}
- for my $k (sort keys $ifaces->%*) {
- my $type = $ifaces->{$k}->{type};
- my $is_bridge = $type eq 'bridge' || $type eq 'OVSBridge';
- my $bridge_match = $is_bridge && $tfilter =~ /^any(_local)?_bridge$/;
- my $match = $tfilter eq $type || $bridge_match;
- delete $ifaces->{$k} if !$match;
+ if ($have_sdn && $tfilter =~ /^(include_sdn|fabric)$/) {
+ my $local_node = PVE::INotify::nodename();
+
+ $fabrics = PVE::Network::SDN::Fabrics::config(1)
+ ->get_interfaces_for_node($local_node);
+ }
+
+ if ($tfilter ne 'include_sdn') {
+ for my $k (sort keys $ifaces->%*) {
+ my $type = $ifaces->{$k}->{type};
+ my $is_bridge = $type eq 'bridge' || $type eq 'OVSBridge';
+ my $bridge_match = $is_bridge && $tfilter =~ /^any(_local)?_bridge$/;
+ my $match = $tfilter eq $type || $bridge_match;
+ delete $ifaces->{$k} if !$match;
+ }
}
if (defined($vnets)) {
$ifaces->{$_} = $vnets->{$_} for keys $vnets->%*
}
+
+ if (defined($fabrics)) {
+ for my $fabric_id (keys %$fabrics) {
+ next if !$rpcenv->check_any(
+ $authuser,
+ "/sdn/fabrics/$fabric_id",
+ ['SDN.Audit', 'SDN.Use', 'SDN.Allocate'],
+ 1
+ );
+
+ $ifaces->{$fabric_id} = $fabrics->{$fabric_id};
+ }
+ }
}
#always check bridge access
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-manager v3 16/18] ui: add sdn networks to ceph / migration
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (69 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 15/18] api: network: add include_sdn / fabric type Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 17/18] ui: sdn: add evpn controller fabric integration Stefan Hanreich
` (3 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
Pass the 'include_sdn' type to the network selectors used in the
datacenter migration settings panel, as well as the ceph wizard, to
enable users to select SDN Vnets, as well as fabrics in the UI.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
www/manager6/ceph/CephInstallWizard.js | 2 ++
www/manager6/dc/OptionView.js | 1 +
2 files changed, 3 insertions(+)
diff --git a/www/manager6/ceph/CephInstallWizard.js b/www/manager6/ceph/CephInstallWizard.js
index ad7dac68c..ba361548c 100644
--- a/www/manager6/ceph/CephInstallWizard.js
+++ b/www/manager6/ceph/CephInstallWizard.js
@@ -477,6 +477,7 @@ Ext.define('PVE.ceph.CephInstallWizard', {
value: '',
fieldLabel: 'Public Network IP/CIDR',
autoSelect: false,
+ type: 'include_sdn',
bind: {
allowBlank: '{configuration}',
},
@@ -490,6 +491,7 @@ Ext.define('PVE.ceph.CephInstallWizard', {
fieldLabel: 'Cluster Network IP/CIDR',
allowBlank: true,
autoSelect: false,
+ type: 'include_sdn',
emptyText: gettext('Same as Public Network'),
cbind: {
nodename: '{nodename}',
diff --git a/www/manager6/dc/OptionView.js b/www/manager6/dc/OptionView.js
index 930500a61..e9a4f12d6 100644
--- a/www/manager6/dc/OptionView.js
+++ b/www/manager6/dc/OptionView.js
@@ -118,6 +118,7 @@ Ext.define('PVE.dc.OptionView', {
editable: true,
notFoundIsValid: true,
vtype: 'IP64CIDRAddress',
+ type: 'include_sdn',
}],
});
me.add_inputpanel_row('ha', gettext('HA Settings'), {
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-manager v3 17/18] ui: sdn: add evpn controller fabric integration
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (70 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 16/18] ui: add sdn networks to ceph / migration Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 18/18] ui: sdn: vxlan: add fabric property Stefan Hanreich
` (2 subsequent siblings)
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
Expose the new fabric field added to the EVPN controller in the UI.
Users can now select any fabric in the EVPN controller, instead of
having to specify peers manually. This simplifies setting up an EVPN
zone via SDN fabrics considerably.
Since the peers field can now be empty, we have to adapt the existing
field to allow empty values and properly send the delete property when
updating a controller.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
www/manager6/sdn/controllers/Base.js | 17 ++++++++++++
www/manager6/sdn/controllers/EvpnEdit.js | 35 ++++++++++++++++++++++--
2 files changed, 50 insertions(+), 2 deletions(-)
diff --git a/www/manager6/sdn/controllers/Base.js b/www/manager6/sdn/controllers/Base.js
index fd8bd9a05..f75080e4e 100644
--- a/www/manager6/sdn/controllers/Base.js
+++ b/www/manager6/sdn/controllers/Base.js
@@ -8,8 +8,25 @@ Ext.define('PVE.panel.SDNControllerBase', {
if (me.isCreate) {
values.type = me.type;
+ delete values.delete;
} else {
delete values.controller;
+
+ for (const [key, value] of Object.entries(values)) {
+ if (value === null || value === undefined || value === "") {
+ delete values[key];
+
+ if (values.delete) {
+ if (Array.isArray(values.delete)) {
+ values.delete.push(key);
+ } else {
+ values.delete = [values.delete, key];
+ }
+ } else {
+ values.delete = [key];
+ }
+ }
+ }
}
return values;
diff --git a/www/manager6/sdn/controllers/EvpnEdit.js b/www/manager6/sdn/controllers/EvpnEdit.js
index d04b3e544..f9fa0215c 100644
--- a/www/manager6/sdn/controllers/EvpnEdit.js
+++ b/www/manager6/sdn/controllers/EvpnEdit.js
@@ -25,10 +25,41 @@ Ext.define('PVE.sdn.controllers.EvpnInputPanel', {
allowBlank: false,
},
{
- xtype: 'textfield',
+ xtype: 'proxmoxNetworkSelector',
+ name: 'fabric',
+ type: 'fabric',
+ valueField: 'iface',
+ displayField: 'iface',
+ fieldLabel: 'SDN Fabric',
+ allowBlank: true,
+ skipEmptyText: true,
+ autoSelect: false,
+ emptyText: gettext('used as underlay network'),
+ nodename: 'localhost',
+ listConfig: {
+ width: 600,
+ columns: [
+ {
+ header: gettext('Fabric'),
+ width: 90,
+ dataIndex: 'iface',
+ },
+ {
+
+ header: gettext('CIDR'),
+ dataIndex: 'cidr',
+ hideable: false,
+ flex: 1,
+ },
+ ],
+ },
+ },
+ {
+ xtype: 'proxmoxtextfield',
name: 'peers',
fieldLabel: gettext('Peers'),
- allowBlank: false,
+ allowBlank: true,
+ deleteEmpty: true,
},
];
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-manager v3 18/18] ui: sdn: vxlan: add fabric property
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (71 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 17/18] ui: sdn: add evpn controller fabric integration Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-gui-tests v3 1/1] pve: add sdn/fabrics screenshots Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-docs v3 1/1] fabrics: add initial documentation for sdn fabrics Stefan Hanreich
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
VXLAN zones can now use fabrics instead of having to specify peers
manually. Since the network selector doesn't implement deleteEmpty,
we have to manually handle deleted properties in the VXLAN input
panel.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
www/manager6/sdn/zones/VxlanEdit.js | 52 +++++++++++++++++++++++++++--
1 file changed, 50 insertions(+), 2 deletions(-)
diff --git a/www/manager6/sdn/zones/VxlanEdit.js b/www/manager6/sdn/zones/VxlanEdit.js
index b556790df..575dd867f 100644
--- a/www/manager6/sdn/zones/VxlanEdit.js
+++ b/www/manager6/sdn/zones/VxlanEdit.js
@@ -8,8 +8,25 @@ Ext.define('PVE.sdn.zones.VxlanInputPanel', {
if (me.isCreate) {
values.type = me.type;
+ delete values.delete;
} else {
delete values.zone;
+
+ for (const [key, value] of Object.entries(values)) {
+ if (value === null || value === undefined || value === "") {
+ delete values[key];
+
+ if (values.delete) {
+ if (Array.isArray(values.delete)) {
+ values.delete.push(key);
+ } else {
+ values.delete = [values.delete, key];
+ }
+ } else {
+ values.delete = [key];
+ }
+ }
+ }
}
delete values.mode;
@@ -22,10 +39,41 @@ Ext.define('PVE.sdn.zones.VxlanInputPanel', {
me.items = [
{
- xtype: 'textfield',
+ xtype: 'proxmoxtextfield',
name: 'peers',
fieldLabel: gettext('Peer Address List'),
- allowBlank: false,
+ allowBlank: true,
+ deleteEmpty: true,
+ },
+ {
+ xtype: 'proxmoxNetworkSelector',
+ name: 'fabric',
+ type: 'fabric',
+ valueField: 'iface',
+ displayField: 'iface',
+ fieldLabel: 'SDN Fabric',
+ skipEmptyText: true,
+ allowBlank: true,
+ autoSelect: false,
+ emptyText: gettext('used as underlay network'),
+ nodename: 'localhost',
+ listConfig: {
+ width: 600,
+ columns: [
+ {
+ header: gettext('Fabric'),
+ width: 90,
+ dataIndex: 'iface',
+ },
+ {
+
+ header: gettext('CIDR'),
+ dataIndex: 'cidr',
+ hideable: false,
+ flex: 1,
+ },
+ ],
+ },
},
];
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-gui-tests v3 1/1] pve: add sdn/fabrics screenshots
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (72 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 18/18] ui: sdn: vxlan: add fabric property Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-docs v3 1/1] fabrics: add initial documentation for sdn fabrics Stefan Hanreich
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
Add a few screenshots for the sdn->fabrics panel:
* fabric overview
* openfabric fabric creation
* ospf fabric creation
* openfabric node creation
* ospf node creation
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
create_fabrics_screenshots | 198 +++++++++++++++++++++++++++++++++++++
1 file changed, 198 insertions(+)
create mode 100755 create_fabrics_screenshots
diff --git a/create_fabrics_screenshots b/create_fabrics_screenshots
new file mode 100755
index 0000000..a83bb22
--- /dev/null
+++ b/create_fabrics_screenshots
@@ -0,0 +1,198 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use lib '.';
+
+use Carp;
+use Time::HiRes qw(usleep);
+use Data::Dumper;
+use PVE::GUITester;
+use Proxmox::GUITester;
+
+sub prepare_fabrics {
+ my ($conn) = @_;
+ eval {
+ # might not exist, so ignore any errors
+ $conn->delete("/cluster/sdn/fabrics/fabric/test2");
+ $conn->delete("/cluster/sdn/fabrics/fabric/test1");
+ };
+
+ my $openfabric = {
+ id => "test1",
+ ip_prefix => "192.0.2.0/24",
+ hello_interval => "1",
+ protocol => "openfabric",
+ };
+ my $ospf = {
+ id => "test2",
+ area => "0.0.0.0",
+ ip_prefix => "198.51.100.0/24",
+ protocol => "ospf",
+ };
+ $conn->post("/cluster/sdn/fabrics/fabric", $openfabric);
+ $conn->post("/cluster/sdn/fabrics/fabric", $ospf);
+}
+
+sub prepare_nodes {
+ my ($conn) = @_;
+ eval {
+ # might not exist, so ignore any errors
+ $conn->delete("/cluster/sdn/fabrics/node/test2/pve0");
+ $conn->delete("/cluster/sdn/fabrics/node/test2/pve1");
+ $conn->delete("/cluster/sdn/fabrics/node/test2/pve2");
+ $conn->delete("/cluster/sdn/fabrics/node/test1/pve0");
+ $conn->delete("/cluster/sdn/fabrics/node/test1/pve1");
+ $conn->delete("/cluster/sdn/fabrics/node/test1/pve2");
+ };
+
+ my @nodes = ('pve0', 'pve1', 'pve2');
+
+ for my $i (0 .. $#nodes) {
+ my $node_name = $nodes[$i];
+ my $last_octet = $i + 1;
+ my $ospf_iface = ($i * 2) + 19;
+ my $openfabric_iface = ($i * 2) + 25;
+
+ my $ospf_node = {
+ node_id => $node_name,
+ ip => "198.51.100.$last_octet",
+ interfaces => [
+ "name=ens$ospf_iface",
+ "name=ens" . ($ospf_iface + 1 . "")
+ ],
+ protocol => "ospf",
+ };
+ my $openfabric_node = {
+ node_id => $node_name,
+ ip => "192.0.2.$last_octet",
+ interfaces => [
+ "name=ens$openfabric_iface",
+ "name=ens" . ($openfabric_iface + 1)
+ ],
+ protocol => "openfabric",
+ };
+
+ $conn->post("/cluster/sdn/fabrics/node/test1/", $openfabric_node);
+ $conn->post("/cluster/sdn/fabrics/node/test2/", $ospf_node);
+ }
+}
+
+sub open_add_node_panel {
+ my ($self, $protocol) = @_;
+
+ my $driver = $self->{driver};
+
+ my $js = "let panel = Ext.ComponentQuery.query('pveSDNFabricView')[0];" .
+ "panel.controller.openNodeAddWindow({children: [], protocol: '$protocol'});" .
+ "return true";
+
+ my $res;
+
+ $res = Proxmox::GUITester::verify_scalar_result($driver->execute_script($js));
+ croak "unable to open node panel for '$protocol'\n" if !$res;
+}
+
+sub select_interfaces {
+ my ($self, $iface1, $iface2) = @_;
+
+ my $driver = $self->{driver};
+
+ my $js = "let panel = Ext.ComponentQuery.query('pveSDNFabricsInterfacePanel')[0];" .
+ "let store = panel.getStore();" .
+ "let record1 = store.findRecord('name', '$iface1');" .
+ "let record2 = store.findRecord('name', '$iface2');" .
+ "panel.setSelection([record1, record2]);" .
+ "return true";
+
+ my $res;
+
+ $res = Proxmox::GUITester::verify_scalar_result($driver->execute_script($js));
+ croak "unable to select interface '$iface1' or '$iface2'\n" if !$res;
+}
+
+sub create_fabrics_ui_screenshots {
+ my ($gui, $conn) = @_;
+
+ $gui->select_tree_item("root", 10);
+
+ my $panel = $gui->component_query_single('pvePanelConfig');
+ $gui->select_config_item($panel, 'sdnfabrics');
+
+ # get fabric edit window
+ my $menu = $gui->find_button('Add Fabric', $panel)->click();
+ $gui->find_menu_item('OpenFabric')->click();
+
+ my $window = $gui->find_dialog("Create: OpenFabric");
+ $gui->setValue($window, 'id', 'test1');
+ $gui->setValue($window, 'ip_prefix', '192.0.2.0/24');
+ $gui->setValue($window, 'hello_interval', '1');
+
+ $gui->element_screenshot("gui-datacenter-create-fabric-openfabric.png", $window);
+ $gui->window_close($window);
+
+ $menu = $gui->find_button('Add Fabric', $panel)->click();
+ $gui->find_menu_item('OSPF')->click();
+
+ $window = $gui->find_dialog("Create: OSPF");
+ $gui->setValue($window, 'id', 'test2');
+ $gui->setValue($window, 'area', '0');
+ $gui->setValue($window, 'ip_prefix', '198.51.100.0/24');
+
+ $gui->element_screenshot("gui-datacenter-create-fabric-ospf.png", $window);
+ $gui->window_close($window);
+
+ # get node edit window
+ prepare_fabrics($conn);
+ sleep_ms(250);
+
+ open_add_node_panel($gui, "openfabric");
+ sleep_ms(500);
+
+ $window = $gui->find_dialog("Create: Node");
+ $gui->setValue($window, 'ip', '192.0.2.1');
+ select_interfaces($gui, "ens19", "ens20");
+ $gui->element_screenshot("gui-datacenter-create-node-openfabric.png", $window);
+ $gui->window_close($window);
+
+ open_add_node_panel($gui, "ospf");
+ sleep_ms(500);
+
+ $window = $gui->find_dialog("Create: Node");
+ $gui->setValue($window, 'ip', '198.51.100.1');
+ select_interfaces($gui, "ens19", "ens20");
+ $gui->element_screenshot("gui-datacenter-create-node-ospf.png", $window);
+ $gui->window_close($window);
+
+ # get fabric overview
+ prepare_nodes($conn);
+ sleep_ms(250);
+ $gui->reload();
+
+ $gui->select_tree_item("root", 10);
+
+ $panel = $gui->component_query_single('pvePanelConfig');
+ $gui->select_config_item($panel, 'sdnfabrics');
+ $gui->element_screenshot("gui-datacenter-fabrics-overview.png", $panel);
+}
+
+my $gui;
+
+eval {
+
+ local $SIG{TERM} = $SIG{QUIT} = $SIG{INT} = sub { die "got interrupt"; };
+
+ $gui = PVE::GUITester->new(login => 1);
+
+ my $conn = $gui->apiclient();
+
+ create_fabrics_ui_screenshots($gui, $conn);
+};
+my $err = $@;
+
+$gui->quit() if $gui;
+
+die $err if $err;
+
+exit(0);
+
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
* [pve-devel] [PATCH pve-docs v3 1/1] fabrics: add initial documentation for sdn fabrics
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
` (73 preceding siblings ...)
2025-05-22 16:17 ` [pve-devel] [PATCH pve-gui-tests v3 1/1] pve: add sdn/fabrics screenshots Stefan Hanreich
@ 2025-05-22 16:17 ` Stefan Hanreich
74 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-05-22 16:17 UTC (permalink / raw)
To: pve-devel
From: Gabriel Goller <g.goller@proxmox.com>
Add initial documentation for the SDN fabrics, as well as additional
documentation for all available protocols, Openfabric and OSPF. The
screenshots are generated using pve-gui-tests.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pvesdn.adoc | 227 ++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 227 insertions(+)
diff --git a/pvesdn.adoc b/pvesdn.adoc
index 5e58cc3..d8011c3 100644
--- a/pvesdn.adoc
+++ b/pvesdn.adoc
@@ -302,6 +302,9 @@ Peers Address List:: A list of IP addresses of each node in the VXLAN zone. This
can be external nodes reachable at this IP address.
All nodes in the cluster need to be mentioned here.
+SDN Fabric:: Instead of manually defining all the peers, use a
+ xref:pvesdn_config_fabrics[Fabric] for automatically generating the peer list.
+
MTU:: Because VXLAN encapsulation uses 50 bytes, the MTU needs to be 50 bytes
lower than the outgoing physical interface.
@@ -459,6 +462,9 @@ ASN #:: A unique BGP ASN number. It's highly recommended to use a private ASN
number (64512 – 65534, 4200000000 – 4294967294), as otherwise you could end up
breaking global routing by mistake.
+SDN Fabric:: A xref:pvesdn_config_fabrics[Fabric] that contains all the nodes
+ part of the EVPN zone. Will be used as the underlay network.
+
Peers:: An IP list of all nodes that are part of the EVPN zone. (could also be
external nodes or route reflector servers)
@@ -519,6 +525,227 @@ Loopback:: Use a loopback or dummy interface as the source of the EVPN network
(for multipath).
+[[pvesdn_config_fabrics]]
+Fabrics
+-------
+
+[thumbnail="screenshot/gui-datacenter-fabrics-overview.png"]
+
+Fabrics in {pve} SDN provide automated routing between nodes in a cluster. They
+simplify the configuration of underlay networks between nodes to form the
+foundation for SDN deployments.
+
+They automatically configure routing protocols on your physical network
+interfaces to establish connectivity between nodes in the cluster. This creates
+a resilient, auto-configuring network fabric that adapts to changes in network
+topology. These fabrics can be used as a full-mesh network for Ceph
+or in the EVPN controller and VXLAN zone.
+
+Installation
+~~~~~~~~~~~~
+
+The FRR implementations of OpenFabric and OSPF are used, so first ensure that
+the `frr` and `frr-pythontools` packages are installed:
+
+----
+apt update
+apt install frr frr-pythontools
+----
+
+Permissions
+~~~~~~~~~~~
+
+To view the configuration of an SDN fabric users need SDN.Audit or SDN.Allocate
+permissions. To create or modify a fabric configuration, users need SDN.Allocate
+permissions. To view the configuration of a node, users need the Sys.Audit or
+Sys.Modify permissions. When adding or updating nodes within a fabric,
+additional Sys.Modify permission for the specific node is required, since this
+operation involves writing to the node's /etc/network/interfaces file.
+
+Configuration
+~~~~~~~~~~~~~
+
+To create a Fabric, head over to Datacenter->SDN->Fabrics and click "Add
+Fabric". After selecting the preferred protocol, the fabric is created. With
+the "+" button you can select the nodes which you want to add to the fabric,
+you also have to select the interfaces used to communicate with the other nodes.
+
+Loopback Prefix
+^^^^^^^^^^^^^^^
+
+You can specify a CIDR network range (e.g., 192.0.2.0/24) as a loopback prefix for the fabric.
+When configured, the system will automatically verify that all router-IDs are contained within
+this prefix. This ensures consistency in your addressing scheme and helps prevent addressing
+conflicts or errors.
+
+Router-ID Selection
+^^^^^^^^^^^^^^^^^^^
+
+Each node in a fabric needs a unique router-ID, which is an IPv4 address in
+dotted decimal notation (e.g., 192.0.2.1). In OpenFabric this can also be an
+IPv6 address in the typical hexadecimal representation separated by colons
+(e.g., 2001:db8::1428:57ab). A dummy interface with the router-ID as address
+will automatically be created and will act as a loopback interface for the
+fabric (it's also passive by default).
+
+RouteMaps
+^^^^^^^^^
+
+For every fabric, an access-list and a route-map are automatically created. These
+configure the router to rewrite the source address of outgoing packets. When you
+communicate with another node (for example, by pinging it), this ensures that
+traffic originates from the local dummy interface's IP address rather than from
+the physical interface. This provides consistent routing behavior and proper
+source address selection throughout the fabric.
+
+[[pvesdn_openfabric]]
+OpenFabric
+~~~~~~~~~~
+
+OpenFabric is a routing protocol specifically designed for data center fabrics.
+It's based on IS-IS and optimized for the spine-leaf topology common in data
+centers.
+
+[thumbnail="screenshot/gui-datacenter-create-fabric-openfabric.png"]
+
+Configuration options:
+
+[[pvesdn_openfabric_fabric]]
+On the Fabric
+^^^^^^^^^^^^^
+
+Name:: This is the name of the OpenFabric fabric and can be at most 8 characters long.
+
+IPv4 Prefix:: IPv4 CIDR network range (e.g., 192.0.2.0/24) used to verify that
+all router-IDs in the fabric are contained within this prefix.
+
+IPv6 Prefix:: IPv6 CIDR network range (e.g., 2001:db8::/64) used to verify that
+all router-IDs in the fabric are contained within this prefix.
+
+Hello Interval:: Controls how frequently (in seconds) hello packets are sent to
+discover and maintain connections with neighboring nodes. Lower values detect
+failures faster but increase network traffic. This option is global on the
+fabric, meaning every interface on every node in this fabric will inherit this
+hello-interval property. The default value is 3 seconds.
+
+CSNP Interval::: Sets how frequently (in seconds) the node synchronizes its
+routing database with neighbors. Lower values keep the network topology information
+more quickly in sync but increase network traffic. This option is global on the
+fabric, meaning every interface on every node in this fabric will inherit this
+property. The default value is 10 seconds.
+
+[[pvesdn_openfabric_node]]
+On the Node
+^^^^^^^^^^^
+
+[thumbnail="screenshot/gui-datacenter-create-node-openfabric.png"]
+
+Options that are available on every node that is part of a fabric:
+
+Node:: Select the node which will be added to the fabric. Only nodes that
+currently are in the cluster will be shown.
+
+IPv4:: A unique IPv4 address used to generate the OpenFabric
+Network Entity Title (NET). Each node in the same fabric must have a different
+Router-ID, while a single node must use the same NET address across all fabrics
+(If this is not given {pve} will automatically choose one and ensure that the
+configuration is valid).
+
+IPv6:: A unique IPv6 address used to generate the OpenFabric
+Network Entity Title (NET). Each node in the same fabric must have a different
+Router-ID, while a single node must use the same NET address across all fabrics.
+If a IPv4 and IPv6 address is configured, the IPv4 one will be used to derive
+the NET.
+
+WARNING: When using IPv6 addresses, the last 3 segments are used to generate
+the NET. Ensure these segments differ between nodes.
+
+Interfaces:: Specify the interfaces used to establish peering connections with
+other OpenFabric nodes. Preferably select interfaces without pre-assigned IP
+addresses, then configure addresses in the IPv4/IPv6 column if needed. A dummy
+"loopback" interface with the router-id is automatically created.
+
+On The Interface
+^^^^^^^^^^^^^^^^
+
+The following optional parameters can be configured per interface when enabling
+the additional columns:
+
+IP::: A IPv4 that should get automatically configured on this interface. Must
+include the netmask (e.g. /31)
+
+IPv6::: A IPv6 that should get automatically configured on this interface. Must
+include the netmask (e.g. /127).
+
+Hello Multiplier::: Defines how many missed hello packets constitute a failed
+connection. Higher values make the connection more resilient to packet loss but
+slow down failure detection. The default value is 10.
+
+WARNING: When you remove an interface with an entry in `/etc/network/interfaces`
+that has `manual` set, then the IP will not get removed on applying the SDN
+configuration.
+
+[[pvesdn_ospf]]
+OSPF
+~~~~
+
+OSPF (Open Shortest Path First) is a widely-used link-state routing protocol
+that efficiently calculates the shortest path for routing traffic through IP
+networks.
+
+[thumbnail="screenshot/gui-datacenter-create-fabric-ospf.png"]
+
+Configuration options:
+
+[[pvesdn_ospf_fabric]]
+On the Fabric
+^^^^^^^^^^^^^
+
+Area:: This specifies the OSPF area identifier, which can be either a 32-bit
+signed integer or an IP address. Areas are a way to organize and structure OSPF
+networks hierarchically, with Area 0 (or 0.0.0.0) serving as the backbone area.
+
+IPv4 Prefix:: IPv4 CIDR network range (e.g., 192.0.2.0/24) used to
+verify that all router-IDs in the fabric are contained within this prefix.
+
+Area:: This specifies the OSPF area identifier, which can be either an 32-bit
+signed integer or an IP address. Areas are a way to organize and structure OSPF
+networks hierarchically, with Area 0 (or 0.0.0.0) serving as the backbone area.
+
+[[pvesdn_ospf_node]]
+On the Node
+^^^^^^^^^^^
+
+[thumbnail="screenshot/gui-datacenter-create-node-ospf.png"]
+
+Options that are available on every node that is part of a fabric:
+
+Node:: Select the node which will be added to the fabric. Only nodes that
+are currently in the cluster will be shown.
+
+IPv4:: A unique Router-ID used to identify this router within the OSPF
+network. Each node in the same fabric must have a different Router-ID.
+
+Interfaces:: Specify the interfaces used to establish peering connections with
+other OSPF nodes. Preferably select interfaces without pre-assigned IP
+addresses, then configure addresses in the IPv4 column if needed. A dummy
+"loopback" interface with the router-id is automatically created.
+
+On The Interface
+^^^^^^^^^^^^^^^^
+The following optional parameter can be configured per interface:
+
+IP::: A IPv4 that should get automatically configured on this interface. Must
+include the netmask (e.g. /31)
+
+WARNING: When you remove an interface with an entry in `/etc/network/interfaces`
+that has `manual` set, then the IP will not get removed on applying the SDN
+configuration.
+
+NOTE: The dummy interface will automatically be configured as `passive`. Every
+interface which doesn't have an ip-address configured will be treated as a
+`point-to-point` link.
+
[[pvesdn_config_ipam]]
IPAM
----
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] 76+ messages in thread
end of thread, other threads:[~2025-05-22 16:35 UTC | newest]
Thread overview: 76+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-05-22 16:16 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -firewall, -ve-rs, -perl-rs, -widget-toolkit} v3 00/75] Add SDN Fabrics Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox v3 1/4] network-types: initial commit Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox v3 2/4] network-types: make cidr and mac-address types usable by the api Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox v3 3/4] network-types: add api types for ipv4/6 Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox v3 4/4] api-macro: add allof schema to enum Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-firewall v3 1/1] firewall: nftables: migrate to proxmox-network-types Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 01/21] config: use proxmox_serde perl helpers Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 02/21] ve-config: move types to proxmox-network-types Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 03/21] sdn-types: initial commit Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 04/21] frr: create proxmox-frr crate Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 05/21] frr: add common frr types Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 06/21] frr: add openfabric types Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 07/21] frr: add ospf types Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 08/21] frr: add route-map types Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 09/21] frr: add generic types over openfabric and ospf Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 10/21] config: sdn: fabrics: add section types Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 11/21] config: sdn: fabrics: add node " Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 12/21] config: sdn: fabrics: add interface name struct Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 13/21] config: sdn: fabrics: add openfabric properties Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 14/21] config: sdn: fabrics: add ospf properties Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 15/21] config: sdn: fabrics: add api types Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 16/21] config: sdn: fabrics: add section config Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 17/21] config: sdn: fabrics: add fabric config Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 18/21] common: sdn: fabrics: implement validation Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 19/21] sdn: fabrics: config: add conversion from / to section config Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 20/21] sdn: fabrics: implement FRR configuration generation Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-ve-rs v3 21/21] ve-config: add integrations tests Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-perl-rs v3 1/5] pve-rs: Add PVE::RS::SDN::Fabrics module Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-perl-rs v3 2/5] pve-rs: sdn: fabrics: add api methods Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-perl-rs v3 3/5] pve-rs: sdn: fabrics: add frr config generation Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-perl-rs v3 4/5] pve-rs: sdn: fabrics: add helper to generate ifupdown2 configuration Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH proxmox-perl-rs v3 5/5] pve-rs: sdn: fabrics: add helper for network API endpoint Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH pve-cluster v3 1/1] cfs: add fabrics.cfg to observed files Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH pve-access-control v3 1/1] permissions: add ACL paths for SDN fabrics Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 01/21] sdn: fix value returned by pending_config Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 02/21] debian: add dependency to proxmox-perl-rs Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 03/21] fabrics: add fabrics module Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 04/21] refactor: controller: move frr methods into helper Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 05/21] frr: add new helpers for reloading frr configuration Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 06/21] controllers: define new api for frr config generation Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 07/21] sdn: add frr config generation helpers Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 08/21] sdn: api: add check for rewriting frr configuration Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 09/21] test: isis: add test for standalone configuration Stefan Hanreich
2025-05-22 16:16 ` [pve-devel] [PATCH pve-network v3 10/21] sdn: frr: add daemon status to frr helper Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 11/21] sdn: commit fabrics config to running configuration Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 12/21] fabrics: generate ifupdown configuration Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 13/21] fabrics: add jsonschema for fabrics and nodes Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 14/21] api: fabrics: add root-level module Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 15/21] api: fabrics: add fabric submodule Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 16/21] api: fabrics: add node submodule Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 17/21] api: fabrics: add fabricnode submodule Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 18/21] controller: evpn: add fabrics integration Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 19/21] zone: vxlan: " Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 20/21] test: fabrics: add test cases for ospf and openfabric + evpn Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-network v3 21/21] frr: bump frr config version to 10.2.2 Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH proxmox-widget-toolkit v3 1/1] network selector: add type parameter Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 01/18] api: use new sdn config generation functions Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 02/18] ui: fabrics: add model definitions for fabrics Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 03/18] fabric: add common interface panel Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 04/18] fabric: add OpenFabric interface properties Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 05/18] fabric: add OSPF " Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 06/18] fabric: add generic node edit panel Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 07/18] fabric: add OpenFabric node edit Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 08/18] fabric: add OSPF " Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 09/18] fabric: add generic fabric edit panel Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 10/18] fabric: add OpenFabric " Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 11/18] fabric: add OSPF " Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 12/18] fabrics: Add main FabricView Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 13/18] utils: avoid line-break in pending changes message Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 14/18] ui: permissions: add ACL path for fabrics Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 15/18] api: network: add include_sdn / fabric type Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 16/18] ui: add sdn networks to ceph / migration Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 17/18] ui: sdn: add evpn controller fabric integration Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-manager v3 18/18] ui: sdn: vxlan: add fabric property Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-gui-tests v3 1/1] pve: add sdn/fabrics screenshots Stefan Hanreich
2025-05-22 16:17 ` [pve-devel] [PATCH pve-docs v3 1/1] fabrics: add initial documentation for sdn fabrics Stefan Hanreich
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