public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics
@ 2025-04-04 16:28 Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox v2 1/1] serde: add string_as_bool module for boolean string parsing Gabriel Goller
                   ` (57 more replies)
  0 siblings, 58 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

This series allows the user to add fabrics such as OpenFabric and OSPF over
their clusters.

This series relies on: 
https://lore.proxmox.com/pve-devel/20250404135522.2603272-1-s.hanreich@proxmox.com/T/#mf4cf46c066d856cea819ac3e79d115a290f47466

Overview
========

This series allows the user to create routed networks ('fabrics') across their
clusters, which can be used as the underlay network for a EVPN cluster, or for
creating Ceph full mesh clusters easily.

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. There are also plans for adding a new Wireguard fabric to
this.


Implementation
==============

Every fabric consists of one or more nodes, which themselves consists of one or
more interfaces. Fabrics and nodes are modeled as different section config
types, interfaces are an array contained in a node section. We have a separate
configuration file for each fabric type. This is because the basic structure
(fabric, node, interface) is the same, but the specific options vary wildly.
This makes serialization / validation from the Rust side a lot easier.

For now, nodes in the fabric configuration are always PVE nodes, but in the
future nodes could also represent external members of the fabric (e.g. in a
potential wireguard fabric).

Settings can be configured on a fabric-level, so they apply to all interfaces,
or overridden on a interface-level (hidden in the UI by default).

Most of the functionality is implemented by 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.

The API provides one common GET method, to get the configuration of all
different fabric types (for the tree overview), but otherwise there are separate
CRUD endpoints for every fabric type, to mimic the split of configuration files.
Another upside of this is, that the generated rust structs for the API endpoints
(for PDM) will be much nicer.

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, ..).

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 an intermediate configuration in the initial RFC,
but that turned out to be unwieldy (lots of additional code & conversions).
Because of this we introduced a Validation trait, that handles validation of
section config data.

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.


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
============
pve-manager depends on pve-docs
pve-manager depends on pve-network
pve-network depends on proxmox-perl-rs
pve-network depends on pve-cluster
pve-network depends on pve-access-control
proxmox-perl-rs depends on proxmox-ve-config
proxmox-perl-rs depends on proxmox-frr
proxmox-perl-rs depends on proxmox-network-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-frr depends on proxmox-network-types
proxmox-frr depends on proxmox-sdn-types
proxmox-ve-config depends on proxmox-serde

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 

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 Stefan Hanreich for his help and support throughout this series!

proxmox:

Gabriel Goller (1):
  serde: add string_as_bool module for boolean string parsing

 proxmox-serde/src/lib.rs | 84 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 84 insertions(+)


proxmox-ve-rs:

Gabriel Goller (14):
  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
  frr: add serializer for all FRR types
  ve-config: add common section-config types for OpenFabric and OSPF
  ve-config: add openfabric section-config
  ve-config: add ospf section-config
  ve-config: add FRR conversion helpers for openfabric and ospf
  ve-config: add validation for section-config
  ve-config: add section-config to frr types conversion
  ve-config: add integrations tests

Stefan Hanreich (1):
  sdn-types: initial commit

 Cargo.toml                                    |   9 +
 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                        | 218 +++++++
 proxmox-frr/src/openfabric.rs                 |  93 +++
 proxmox-frr/src/ospf.rs                       | 135 +++++
 proxmox-frr/src/route_map.rs                  | 128 ++++
 proxmox-frr/src/serializer.rs                 | 192 ++++++
 proxmox-sdn-types/Cargo.toml                  |  14 +
 proxmox-sdn-types/debian/changelog            |   5 +
 proxmox-sdn-types/debian/control              |  39 ++
 proxmox-sdn-types/debian/copyright            |  18 +
 proxmox-sdn-types/debian/debcargo.toml        |   7 +
 proxmox-sdn-types/src/lib.rs                  |   2 +
 proxmox-sdn-types/src/net.rs                  | 382 ++++++++++++
 proxmox-sdn-types/src/openfabric.rs           |  89 +++
 proxmox-ve-config/Cargo.toml                  |  19 +-
 proxmox-ve-config/debian/control              |  33 +-
 proxmox-ve-config/src/sdn/fabric/mod.rs       | 571 ++++++++++++++++++
 .../src/sdn/fabric/openfabric/frr.rs          |  24 +
 .../src/sdn/fabric/openfabric/mod.rs          | 229 +++++++
 .../src/sdn/fabric/openfabric/validation.rs   |  58 ++
 proxmox-ve-config/src/sdn/fabric/ospf/frr.rs  |  21 +
 proxmox-ve-config/src/sdn/fabric/ospf/mod.rs  | 220 +++++++
 .../src/sdn/fabric/ospf/validation.rs         |  68 +++
 proxmox-ve-config/src/sdn/mod.rs              |   1 +
 .../tests/fabric/cfg/openfabric_default.cfg   |  24 +
 .../cfg/openfabric_loopback_prefix_fail.cfg   |  24 +
 .../fabric/cfg/openfabric_multi_fabric.cfg    |  33 +
 .../cfg/openfabric_verification_fail.cfg      |  16 +
 .../tests/fabric/cfg/ospf_default.cfg         |  16 +
 ...f_interface_properties_validation_fail.cfg |  10 +
 .../fabric/cfg/ospf_loopback_prefix_fail.cfg  |  23 +
 .../tests/fabric/cfg/ospf_multi_fabric.cfg    |  33 +
 .../fabric/cfg/ospf_verification_fail.cfg     |  17 +
 proxmox-ve-config/tests/fabric/helper.rs      |  43 ++
 proxmox-ve-config/tests/fabric/main.rs        | 132 ++++
 .../fabric__openfabric_default_pve.snap       |  36 ++
 .../fabric__openfabric_default_pve1.snap      |  32 +
 .../fabric__openfabric_multi_fabric_pve1.snap |  48 ++
 .../snapshots/fabric__ospf_default_pve.snap   |  31 +
 .../snapshots/fabric__ospf_default_pve1.snap  |  27 +
 .../fabric__ospf_multi_fabric_pve1.snap       |  48 ++
 46 files changed, 3264 insertions(+), 6 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/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/sdn/fabric/mod.rs
 create mode 100644 proxmox-ve-config/src/sdn/fabric/openfabric/frr.rs
 create mode 100644 proxmox-ve-config/src/sdn/fabric/openfabric/mod.rs
 create mode 100644 proxmox-ve-config/src/sdn/fabric/openfabric/validation.rs
 create mode 100644 proxmox-ve-config/src/sdn/fabric/ospf/frr.rs
 create mode 100644 proxmox-ve-config/src/sdn/fabric/ospf/mod.rs
 create mode 100644 proxmox-ve-config/src/sdn/fabric/ospf/validation.rs
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/openfabric_default.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/openfabric_loopback_prefix_fail.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/openfabric_multi_fabric.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/openfabric_verification_fail.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/ospf_default.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/ospf_interface_properties_validation_fail.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/ospf_loopback_prefix_fail.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/ospf_multi_fabric.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/ospf_verification_fail.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_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:

Gabriel Goller (7):
  perl-rs: sdn: initial fabric infrastructure
  perl-rs: sdn: add CRUD helpers for OpenFabric fabric management
  perl-rs: sdn: OpenFabric perlmod methods
  perl-rs: sdn: implement Openfabric interface file generation
  perl-rs: sdn: add CRUD helpers for OSPF fabric management
  perl-rs: sdn: OSPF perlmod methods
  perl-rs: sdn: implement OSPF interface file configuration generation

 pve-rs/Cargo.toml            |   7 +-
 pve-rs/Makefile              |   3 +
 pve-rs/debian/control        |   6 +
 pve-rs/src/lib.rs            |   1 +
 pve-rs/src/sdn/fabrics.rs    |  50 ++++
 pve-rs/src/sdn/mod.rs        |   3 +
 pve-rs/src/sdn/openfabric.rs | 462 +++++++++++++++++++++++++++++++++++
 pve-rs/src/sdn/ospf.rs       | 438 +++++++++++++++++++++++++++++++++
 8 files changed, 969 insertions(+), 1 deletion(-)
 create mode 100644 pve-rs/src/sdn/fabrics.rs
 create mode 100644 pve-rs/src/sdn/mod.rs
 create mode 100644 pve-rs/src/sdn/openfabric.rs
 create mode 100644 pve-rs/src/sdn/ospf.rs


pve-cluster:

Gabriel Goller (1):
  cluster: add sdn fabrics config files

 src/PVE/Cluster.pm  | 2 ++
 src/pmxcfs/status.c | 2 ++
 2 files changed, 4 insertions(+)


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 (18):
  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: implement new api for frr config generation
  sdn: add frr config generation helper
  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
  api: fabrics: add common helpers
  api: openfabric: add api endpoints
  api: openfabric: add node endpoints
  api: ospf: add fabric endpoints
  api: ospf: add node endpoints
  api: fabrics: add module / subfolder
  test: fabrics: add test cases for ospf and openfabric + evpn
  frr: bump frr config version to 10.2.1

 debian/control                                |   3 +
 src/PVE/API2/Network/SDN.pm                   |   7 +
 src/PVE/API2/Network/SDN/Fabrics.pm           | 208 ++++++++
 src/PVE/API2/Network/SDN/Fabrics/Common.pm    | 126 +++++
 src/PVE/API2/Network/SDN/Fabrics/Makefile     |   9 +
 src/PVE/API2/Network/SDN/Fabrics/OSPF.pm      | 155 ++++++
 src/PVE/API2/Network/SDN/Fabrics/OSPFNode.pm  | 163 ++++++
 .../API2/Network/SDN/Fabrics/OpenFabric.pm    | 125 +++++
 .../Network/SDN/Fabrics/OpenFabricNode.pm     | 181 +++++++
 src/PVE/API2/Network/SDN/Makefile             |   3 +-
 src/PVE/Network/SDN.pm                        | 140 +++++-
 src/PVE/Network/SDN/Controllers.pm            |  67 +--
 src/PVE/Network/SDN/Controllers/BgpPlugin.pm  |  21 +-
 src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 295 +----------
 src/PVE/Network/SDN/Controllers/IsisPlugin.pm |  21 +-
 src/PVE/Network/SDN/Controllers/Plugin.pm     |  31 +-
 src/PVE/Network/SDN/Fabrics.pm                | 133 +++++
 src/PVE/Network/SDN/Frr.pm                    | 462 ++++++++++++++++++
 src/PVE/Network/SDN/Makefile                  |   2 +-
 src/PVE/Network/SDN/Zones.pm                  |  10 -
 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                |  72 +++
 .../openfabric_fabric/expected_sdn_interfaces |  56 +++
 .../zones/evpn/openfabric_fabric/interfaces   |   6 +
 .../zones/evpn/openfabric_fabric/sdn_config   |  85 ++++
 .../ospf_fabric/expected_controller_config    |  66 +++
 .../evpn/ospf_fabric/expected_sdn_interfaces  |  53 ++
 src/test/zones/evpn/ospf_fabric/interfaces    |   6 +
 src/test/zones/evpn/ospf_fabric/sdn_config    |  82 ++++
 .../evpn/rt_import/expected_controller_config |   2 +-
 .../evpn/vxlanport/expected_controller_config |   2 +-
 53 files changed, 2221 insertions(+), 474 deletions(-)
 create mode 100644 src/PVE/API2/Network/SDN/Fabrics.pm
 create mode 100644 src/PVE/API2/Network/SDN/Fabrics/Common.pm
 create mode 100644 src/PVE/API2/Network/SDN/Fabrics/Makefile
 create mode 100644 src/PVE/API2/Network/SDN/Fabrics/OSPF.pm
 create mode 100644 src/PVE/API2/Network/SDN/Fabrics/OSPFNode.pm
 create mode 100644 src/PVE/API2/Network/SDN/Fabrics/OpenFabric.pm
 create mode 100644 src/PVE/API2/Network/SDN/Fabrics/OpenFabricNode.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


pve-manager:

Gabriel Goller (4):
  api: use new generalized frr and etc network config helper functions
  fabric: add common interface panel
  fabrics: Add main FabricView
  utils: avoid line-break in pending changes message

Stefan Hanreich (7):
  fabric: add OpenFabric interface properties
  fabric: add OSPF interface properties
  fabric: add generic node edit panel
  fabric: add generic fabric edit panel
  fabric: add OpenFabric fabric edit panel
  fabric: add OSPF fabric edit panel
  ui: permissions: add ACL paths for fabrics

 PVE/API2/Network.pm                           |   6 +-
 www/manager6/Makefile                         |   8 +
 www/manager6/Utils.js                         |   2 +-
 www/manager6/data/PermPathStore.js            |   2 +
 www/manager6/dc/Config.js                     |   8 +
 www/manager6/sdn/FabricsView.js               | 419 ++++++++++++++++++
 www/manager6/sdn/fabrics/Common.js            | 292 ++++++++++++
 www/manager6/sdn/fabrics/FabricEdit.js        |  44 ++
 www/manager6/sdn/fabrics/NodeEdit.js          | 224 ++++++++++
 .../sdn/fabrics/openfabric/FabricEdit.js      |  37 ++
 .../sdn/fabrics/openfabric/InterfacePanel.js  |  64 +++
 www/manager6/sdn/fabrics/ospf/FabricEdit.js   |  40 ++
 .../sdn/fabrics/ospf/InterfacePanel.js        |  27 ++
 13 files changed, 1170 insertions(+), 3 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/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/ospf/FabricEdit.js
 create mode 100644 www/manager6/sdn/fabrics/ospf/InterfacePanel.js


pve-gui-tests:

Gabriel Goller (1):
  pve: add sdn/fabrics screenshots

 create_fabrics_screenshots | 197 +++++++++++++++++++++++++++++++++++++
 1 file changed, 197 insertions(+)
 create mode 100755 create_fabrics_screenshots


pve-docs:

Gabriel Goller (1):
  fabrics: add initial documentation for sdn fabrics

 pvesdn.adoc | 206 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 206 insertions(+)


Summary over all repositories:
  126 files changed, 8117 insertions(+), 484 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 v2 1/1] serde: add string_as_bool module for boolean string parsing
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 01/15] sdn-types: initial commit Gabriel Goller
                   ` (56 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

Add new module to proxmox-serde that parses boolean values from various
string formats. Provides parse_bool function supporting common
representations (0/1, true/false, on/off, yes/no) along with
serializer/deserializer for these formats.

Note: this was copied over from proxmox-ve-config/firewall.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-serde/src/lib.rs | 84 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 84 insertions(+)

diff --git a/proxmox-serde/src/lib.rs b/proxmox-serde/src/lib.rs
index c162834905c2..705ad430c953 100644
--- a/proxmox-serde/src/lib.rs
+++ b/proxmox-serde/src/lib.rs
@@ -260,3 +260,87 @@ pub mod string_as_base64url_nopad {
         String::from_utf8(bytes).map_err(|err| Error::custom(err.to_string()))
     }
 }
+
+/// Parse a bool from a string
+pub mod string_as_bool {
+    use std::fmt;
+
+    use serde::{
+        de::{Deserializer, Error, Visitor},
+        ser::Serializer,
+    };
+
+    /// Parse a boolean.
+    ///
+    /// values that parse as [`false`]: 0, false, off, no
+    /// values that parse as [`true`]: 1, true, on, yes
+    ///
+    /// # Examples
+    /// ```ignore
+    /// assert_eq!(parse_bool("false"), Ok(false));
+    /// assert_eq!(parse_bool("on"), Ok(true));
+    /// assert!(parse_bool("proxmox").is_err());
+    /// ```
+    pub fn parse_bool(value: &str) -> Result<bool, anyhow::Error> {
+        Ok(
+            if value == "0"
+                || value.eq_ignore_ascii_case("false")
+                || value.eq_ignore_ascii_case("off")
+                || value.eq_ignore_ascii_case("no")
+            {
+                false
+            } else if value == "1"
+                || value.eq_ignore_ascii_case("true")
+                || value.eq_ignore_ascii_case("on")
+                || value.eq_ignore_ascii_case("yes")
+            {
+                true
+            } else {
+                anyhow::bail!("not a boolean: {value:?}");
+            },
+        )
+    }
+
+    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> {
+                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)
+    }
+
+    pub fn serialize<S: Serializer>(from: &Option<bool>, serializer: S) -> Result<S::Ok, S::Error> {
+        if *from == Some(true) {
+            serializer.serialize_str("1")
+        } else {
+            serializer.serialize_str("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 proxmox-ve-rs v2 01/15] sdn-types: initial commit
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox v2 1/1] serde: add string_as_bool module for boolean string parsing Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 02/15] frr: create proxmox-frr crate Gabriel Goller
                   ` (55 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

This crate contains SDN specific types, so they can be re-used across
multiple crates (The initial use-case being shared types between
proxmox-frr and proxmox-ve-config).

This initial commit contains types for the following entities:
* OpenFabric Hello Interval/Multiplier and CSNP Interval
* Network Entity Title (used as Router IDs in IS-IS / OpenFabric)

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 Cargo.toml                             |   4 +
 proxmox-sdn-types/Cargo.toml           |  14 +
 proxmox-sdn-types/debian/changelog     |   5 +
 proxmox-sdn-types/debian/control       |  39 +++
 proxmox-sdn-types/debian/copyright     |  18 ++
 proxmox-sdn-types/debian/debcargo.toml |   7 +
 proxmox-sdn-types/src/lib.rs           |   2 +
 proxmox-sdn-types/src/net.rs           | 382 +++++++++++++++++++++++++
 proxmox-sdn-types/src/openfabric.rs    |  89 ++++++
 proxmox-ve-config/Cargo.toml           |   6 +-
 10 files changed, 563 insertions(+), 3 deletions(-)
 create mode 100644 proxmox-sdn-types/Cargo.toml
 create mode 100644 proxmox-sdn-types/debian/changelog
 create mode 100644 proxmox-sdn-types/debian/control
 create mode 100644 proxmox-sdn-types/debian/copyright
 create mode 100644 proxmox-sdn-types/debian/debcargo.toml
 create mode 100644 proxmox-sdn-types/src/lib.rs
 create mode 100644 proxmox-sdn-types/src/net.rs
 create mode 100644 proxmox-sdn-types/src/openfabric.rs

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



_______________________________________________
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 v2 02/15] frr: create proxmox-frr crate
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox v2 1/1] serde: add string_as_bool module for boolean string parsing Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 01/15] sdn-types: initial commit Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 03/15] frr: add common frr types Gabriel Goller
                   ` (54 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

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                       |  3 ++
 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, 103 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 bd6a9ca4a79a..0cc84ecda48e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,7 @@
 [workspace]
 members = [
     "proxmox-ve-config",
+    "proxmox-frr",
     "proxmox-sdn-types",
 ]
 exclude = [
@@ -21,3 +22,5 @@ proxmox-network-types = { version = "0.1" }
 serde = { version = "1" }
 serde_with = "3"
 thiserror = "1.0.59"
+
+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 000000000000..e29453ae09c3
--- /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 000000000000..47d734857469
--- /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 000000000000..5afaf5bfee2b
--- /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 000000000000..1ea8a56b4f58
--- /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 000000000000..87a787e6d03e
--- /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 000000000000..e69de29bb2d1
-- 
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 v2 03/15] frr: add common frr types
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (2 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 02/15] frr: create proxmox-frr crate Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 04/15] frr: add openfabric types Gabriel Goller
                   ` (53 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

Add common frr 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 | 114 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 114 insertions(+)

diff --git a/proxmox-frr/src/lib.rs b/proxmox-frr/src/lib.rs
index e69de29bb2d1..e3986d6f2046 100644
--- a/proxmox-frr/src/lib.rs
+++ b/proxmox-frr/src/lib.rs
@@ -0,0 +1,114 @@
+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 argument to an FRR option must only contain ascii characters and must not be 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())
+        {
+            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,
+}
+
+/// Normal linux interface name. 16-bytes length enforced.
+#[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 v2 04/15] frr: add openfabric types
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (3 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 03/15] frr: add common frr types Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 05/15] frr: add ospf types Gabriel Goller
                   ` (52 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

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 | 93 +++++++++++++++++++++++++++++++++++
 3 files changed, 96 insertions(+)
 create mode 100644 proxmox-frr/src/openfabric.rs

diff --git a/proxmox-frr/debian/control b/proxmox-frr/debian/control
index 5afaf5bfee2b..d5603ab4c96d 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 e3986d6f2046..034b5601f8a7 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 000000000000..5191f34402a9
--- /dev/null
+++ b/proxmox-frr/src/openfabric.rs
@@ -0,0 +1,93 @@
+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;
+use crate::RouterNameError;
+
+#[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)
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, PartialOrd, Ord)]
+pub struct OpenFabricRouter {
+    pub net: Net,
+}
+
+impl OpenFabricRouter {
+    pub fn new(net: Net) -> Self {
+        Self { net }
+    }
+
+    pub fn net(&self) -> &Net {
+        &self.net
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, 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_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 converting router name")]
+    RouterNameError(#[from] RouterNameError),
+    #[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 v2 05/15] frr: add ospf types
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (4 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 04/15] frr: add openfabric types Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 06/15] frr: add route-map types Gabriel Goller
                   ` (51 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

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. So the UI has a simple
"unnumbered" check and we set the NetworkType to "Point-to-Point". The
other options are also not that interesting.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 proxmox-frr/src/lib.rs  |  20 ++++++
 proxmox-frr/src/ospf.rs | 135 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 155 insertions(+)
 create mode 100644 proxmox-frr/src/ospf.rs

diff --git a/proxmox-frr/src/lib.rs b/proxmox-frr/src/lib.rs
index 034b5601f8a7..750d7ea07326 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,25 @@ impl Display for InterfaceName {
     }
 }
 
+/// Generic FRR Interface.
+#[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 000000000000..f41b1c0d400a
--- /dev/null
+++ b/proxmox-frr/src/ospf.rs
@@ -0,0 +1,135 @@
+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. There is only one ospf fabric possible in frr (ignoring
+/// multiple invocations of the ospfd daemon) and the separation is done with areas. Still,
+/// different areas have the same frr router, so the name of the router is just "ospf" in "router
+/// ospf". This type still contains the Area so that we can insert it in the Hashmap.
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, PartialOrd, Ord)]
+pub struct OspfRouterName(Area);
+
+impl From<Area> for OspfRouterName {
+    fn from(value: Area) -> Self {
+        Self(value)
+    }
+}
+
+impl Display for OspfRouterName {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "ospf")
+    }
+}
+
+#[derive(Error, Debug)]
+pub enum AreaParsingError {
+    #[error("Invalid area idenitifier. Area must be a number or an ipv4 address.")]
+    InvalidArea,
+    #[error("Invalid area idenitifier. Missing 'area' prefix.")]
+    MissingPrefix,
+    #[error("Error parsing to FrrWord")]
+    FrrWordError(#[from] FrrWordError),
+}
+
+/// The OSPF Area. Most commonly, this is just a number, e.g. 5, but sometimes also a
+/// pseudo-ipaddress, e.g. 0.0.0.0
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, 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)
+    }
+}
+
+#[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),
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, PartialOrd, Ord)]
+pub enum NetworkType {
+    Broadcast,
+    NonBroadcast,
+    /// If the interface is unnumbered (i.e. no specific ip-address at the interface, but the
+    /// router-id).
+    ///
+    /// 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. 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"),
+        }
+    }
+}
+
+#[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 v2 06/15] frr: add route-map types
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (5 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 05/15] frr: add ospf types Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 07/15] frr: add generic types over openfabric and ospf Gabriel Goller
                   ` (50 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

Only a very limited featureset of the route-maps is implemented here,
only the stuff needed by the fabrics. Once standalone route-maps will
make it into pve, we will build these out and maybe rework them a
little.

We need the RouteMaps for the Fabrics, because otherwise, the routes
between to the route-ids will be distibuted, but won't be pingable,
because because the source address is wrong. By rewriting the source we
guarantee that a ping will always find it's way back to the source
router-id.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 proxmox-frr/src/lib.rs       |   1 +
 proxmox-frr/src/route_map.rs | 128 +++++++++++++++++++++++++++++++++++
 2 files changed, 129 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 750d7ea07326..42f0800c536a 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 000000000000..fd6a741d7039
--- /dev/null
+++ b/proxmox-frr/src/route_map.rs
@@ -0,0 +1,128 @@
+use std::{
+    fmt::{self, Display},
+    net::IpAddr,
+};
+
+use proxmox_network_types::ip_address::Cidr;
+
+#[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"),
+        }
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct AccessListRule {
+    pub action: AccessAction,
+    pub network: Cidr,
+    pub seq: Option<u32>,
+}
+
+#[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)
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct AccessList {
+    pub name: AccessListName,
+    pub rules: Vec<AccessListRule>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum RouteMapMatch {
+    IpAddress(AccessListName),
+    IpNextHop(String),
+}
+
+impl Display for RouteMapMatch {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            RouteMapMatch::IpAddress(name) => write!(f, "match ip address {}", name),
+            RouteMapMatch::IpNextHop(name) => write!(f, "match ip next-hop {}", name),
+        }
+    }
+}
+
+#[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)]
+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)
+    }
+}
+
+#[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>,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+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"),
+        }
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct ProtocolRouteMap {
+    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 v2 07/15] frr: add generic types over openfabric and ospf
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (6 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 06/15] frr: add route-map types Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 08/15] frr: add serializer for all FRR types Gabriel Goller
                   ` (49 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

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.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 proxmox-frr/src/lib.rs | 82 +++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 81 insertions(+), 1 deletion(-)

diff --git a/proxmox-frr/src/lib.rs b/proxmox-frr/src/lib.rs
index 42f0800c536a..e1b0ec8878d2 100644
--- a/proxmox-frr/src/lib.rs
+++ b/proxmox-frr/src/lib.rs
@@ -1,8 +1,9 @@
 pub mod openfabric;
 pub mod ospf;
 pub mod route_map;
-use std::{fmt::Display, str::FromStr};
+use std::{collections::BTreeMap, fmt::Display, str::FromStr};
 
+use crate::route_map::{AccessList, AccessListName, ProtocolRouteMap, RouteMap};
 use serde::{Deserialize, Serialize};
 use serde_with::{DeserializeFromStr, SerializeDisplay};
 use thiserror::Error;
@@ -15,6 +16,46 @@ pub enum RouterNameError {
     FrrWordError(#[from] FrrWordError),
 }
 
+/// Generic FRR router.
+///
+/// This generic FRR router contains all the variants that are allowed.
+#[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.
+///
+/// Can vary between different router-types. Some have `router <protocol> <name>`, others have
+/// `router <protocol> <process-id>`.
+#[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)
+    }
+}
+
+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 we can have two
 /// different entries in the hashmap. This allows us to have an interface in an ospf and openfabric
 /// fabric.
@@ -134,3 +175,42 @@ impl Display for CommonInterfaceName {
         self.0.fmt(f)
     }
 }
+
+/// Main FRR config.
+///
+/// Contains the two main frr building blocks: routers and interfaces. To ease construction use the
+/// [`FrrConfigBuilder`], which converts the intermediate representation to the FRR-specific
+/// representation here. Eventually we can add different control options here, such as: `line vty`.
+#[derive(Clone, Debug, PartialEq, Eq, Default)]
+pub struct FrrConfig {
+    pub router: BTreeMap<RouterName, Router>,
+    pub interfaces: BTreeMap<InterfaceName, Interface>,
+    pub access_lists: BTreeMap<AccessListName, AccessList>,
+    pub routemaps: Vec<RouteMap>,
+    pub protocol_routemaps: Vec<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 = (&AccessListName, &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()
+    }
+}
-- 
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 v2 08/15] frr: add serializer for all FRR types
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (7 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 07/15] frr: add generic types over openfabric and ospf Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 09/15] ve-config: add common section-config types for OpenFabric and OSPF Gabriel Goller
                   ` (48 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

This custom serializer will serialize all the FRR types into a string,
which is the FRR config.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 proxmox-frr/src/lib.rs        |   2 +
 proxmox-frr/src/serializer.rs | 192 ++++++++++++++++++++++++++++++++++
 2 files changed, 194 insertions(+)
 create mode 100644 proxmox-frr/src/serializer.rs

diff --git a/proxmox-frr/src/lib.rs b/proxmox-frr/src/lib.rs
index e1b0ec8878d2..b6fe101dcff1 100644
--- a/proxmox-frr/src/lib.rs
+++ b/proxmox-frr/src/lib.rs
@@ -1,6 +1,8 @@
 pub mod openfabric;
 pub mod ospf;
 pub mod route_map;
+pub mod serializer;
+
 use std::{collections::BTreeMap, fmt::Display, str::FromStr};
 
 use crate::route_map::{AccessList, AccessListName, ProtocolRouteMap, RouteMap};
diff --git a/proxmox-frr/src/serializer.rs b/proxmox-frr/src/serializer.rs
new file mode 100644
index 000000000000..631956aeb099
--- /dev/null
+++ b/proxmox-frr/src/serializer.rs
@@ -0,0 +1,192 @@
+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())?;
+        } else {
+            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)?;
+        }
+        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 {
+        writeln!(
+            f,
+            "ip protocol {} route-map {}",
+            self.protocol, self.routemap_name
+        )?;
+        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 v2 09/15] ve-config: add common section-config types for OpenFabric and OSPF
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (8 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 08/15] frr: add serializer for all FRR types Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 10/15] ve-config: add openfabric section-config Gabriel Goller
                   ` (47 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

Introduce shared types for SDN fabric configuration:
- FabricId: String identifier used in both OpenFabric and OSPF configurations
- NodeId: Composite identifier for fabric nodes consisting of FabricId and Hostname
- SectionType: Enum differentiating between fabric and node sections, automatically
  added as a "type" property

These allow us to be somewhat generic over the protocol type (in e.g.
permissions).

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 proxmox-ve-config/src/sdn/fabric/mod.rs | 111 ++++++++++++++++++++++++
 1 file changed, 111 insertions(+)
 create mode 100644 proxmox-ve-config/src/sdn/fabric/mod.rs

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 000000000000..5b24a3be8c17
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/mod.rs
@@ -0,0 +1,111 @@
+pub mod openfabric;
+pub mod ospf;
+
+use proxmox_network_types::debian::Hostname;
+use proxmox_section_config::typed::ApiSectionDataEntry;
+use proxmox_section_config::typed::SectionConfigData;
+
+use std::ops::Deref;
+
+use serde::de::DeserializeOwned;
+use serde_with::{DeserializeFromStr, SerializeDisplay};
+
+#[derive(
+    SerializeDisplay, DeserializeFromStr, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
+)]
+pub struct FabricId(String);
+
+impl FabricId {
+    pub fn new(id: impl Into<String>) -> Result<Self, anyhow::Error> {
+        let value = id.into();
+        if value.len() <= 8 {
+            Ok(Self(value))
+        } else {
+            anyhow::bail!("FabricId has to be shorter than 8 characters");
+        }
+    }
+}
+
+impl AsRef<str> for FabricId {
+    fn as_ref(&self) -> &str {
+        &self.0
+    }
+}
+
+impl std::str::FromStr for FabricId {
+    type Err = anyhow::Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Self::new(s)
+    }
+}
+
+impl std::fmt::Display for FabricId {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+#[derive(SerializeDisplay, DeserializeFromStr, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct NodeId {
+    pub fabric_id: FabricId,
+    pub node_id: Hostname,
+}
+
+impl std::fmt::Display for NodeId {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}_{}", self.fabric_id, self.node_id)
+    }
+}
+
+impl NodeId {
+    pub fn new(fabric_id: FabricId, node_id: Hostname) -> NodeId {
+        NodeId { fabric_id, node_id }
+    }
+}
+
+impl std::str::FromStr for NodeId {
+    type Err = anyhow::Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if let Some((fabric_id, node_id)) = s.split_once('_') {
+            return Ok(NodeId {
+                fabric_id: fabric_id.parse()?,
+                node_id: node_id.parse()?,
+            });
+        }
+
+        anyhow::bail!("invalid node id");
+    }
+}
+
+#[derive(
+    SerializeDisplay, DeserializeFromStr, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
+)]
+pub enum SectionType {
+    Fabric,
+    Node,
+}
+
+impl std::fmt::Display for SectionType {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let name = match self {
+            Self::Fabric => "fabric",
+            Self::Node => "node",
+        };
+
+        write!(f, "{name}")
+    }
+}
+
+impl std::str::FromStr for SectionType {
+    type Err = anyhow::Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Ok(match s {
+            "fabric" => Self::Fabric,
+            "node" => Self::Node,
+            _ => anyhow::bail!("invalid fabric type: {s}"),
+        })
+    }
+}
-- 
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 v2 10/15] ve-config: add openfabric section-config
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (9 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 09/15] ve-config: add common section-config types for OpenFabric and OSPF Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 11/15] ve-config: add ospf section-config Gabriel Goller
                   ` (46 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

This is the main openfabric configuration. It is used to parse from the
section-config file (`/etc/pve/sdn/fabrics/openfabric.cfg`) and is also
returned from the api.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 Cargo.toml                                    |   4 +-
 proxmox-ve-config/Cargo.toml                  |   5 +-
 proxmox-ve-config/debian/control              |   8 +
 .../src/sdn/fabric/openfabric/mod.rs          | 228 ++++++++++++++++++
 4 files changed, 243 insertions(+), 2 deletions(-)
 create mode 100644 proxmox-ve-config/src/sdn/fabric/openfabric/mod.rs

diff --git a/Cargo.toml b/Cargo.toml
index 0cc84ecda48e..ddd32e6c9f97 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,4 +23,6 @@ serde = { version = "1" }
 serde_with = "3"
 thiserror = "1.0.59"
 
-proxmox-sdn-types = { version = "0.1", path = "proxmox-sdn-types" }
+proxmox-frr = { version = "0.1.0", path = "proxmox-frr" }
+proxmox-sdn-types = { version = "0.1.0", path = "proxmox-sdn-types" }
+proxmox-ve-config = { version = "0.2.2", path = "proxmox-ve-config" }
diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml
index d8735e33653b..f58c6e2d0b8c 100644
--- a/proxmox-ve-config/Cargo.toml
+++ b/proxmox-ve-config/Cargo.toml
@@ -18,6 +18,9 @@ serde_plain = "1"
 serde_with = { workspace = true }
 
 proxmox-network-types = { workspace = true }
-proxmox-schema = "4"
+proxmox-sdn-types = { workspace = true }
+proxmox-schema = { version = "4", features = [ "api-types" ] }
+proxmox-section-config = { version = "2.1.2" }
+proxmox-serde = { version = "0.1.2" }
 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 d4985202caef..0da241fed775 100644
--- a/proxmox-ve-config/debian/control
+++ b/proxmox-ve-config/debian/control
@@ -10,7 +10,11 @@ Build-Depends-Arch: cargo:native <!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+api-types-dev <!nocheck>,
  librust-proxmox-schema-4+default-dev <!nocheck>,
+ librust-proxmox-sdn-types-0.1+default-dev <!nocheck>,
+ librust-proxmox-section-config-2+default-dev (>= 2.1.2-~~) <!nocheck>,
+ librust-proxmox-serde-0.1+default-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>,
@@ -35,7 +39,11 @@ Depends:
  librust-log-0.4+default-dev,
  librust-nix-0.26+default-dev,
  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-2+default-dev (>= 2.1.2-~~),
+ librust-proxmox-serde-0.1+default-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/sdn/fabric/openfabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/openfabric/mod.rs
new file mode 100644
index 000000000000..6e7f4e947a0e
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/openfabric/mod.rs
@@ -0,0 +1,228 @@
+#[cfg(feature = "frr")]
+pub mod frr;
+
+use proxmox_network_types::{
+    debian::Hostname,
+    ip_address::{Cidr, Ipv4Cidr, Ipv6Cidr},
+};
+
+use proxmox_schema::property_string::PropertyString;
+use proxmox_sdn_types::openfabric::{CsnpInterval, HelloInterval, HelloMultiplier};
+use proxmox_sortable_macro::sortable;
+use std::{net::IpAddr, sync::OnceLock};
+
+use proxmox_schema::{
+    api_types::{CIDR_FORMAT, CIDR_V4_FORMAT, CIDR_V6_FORMAT},
+    ApiStringFormat, ApiType, ArraySchema, BooleanSchema, IntegerSchema, ObjectSchema, Schema,
+    StringSchema,
+};
+use proxmox_section_config::{typed::ApiSectionDataEntry, SectionConfig, SectionConfigPlugin};
+use proxmox_serde::string_as_bool;
+use serde::{Deserialize, Serialize};
+
+use crate::sdn::fabric::{FabricId, NodeId, SectionType};
+
+#[sortable]
+const FABRIC_SCHEMA: ObjectSchema = ObjectSchema::new(
+    "fabric schema",
+    &sorted!([
+        ("fabric_id", false, &StringSchema::new("FabricId").schema()),
+        (
+            "hello_interval",
+            true,
+            &IntegerSchema::new("OpenFabric hello_interval in seconds")
+                .minimum(1)
+                .maximum(600)
+                .schema(),
+        ),
+        (
+            "loopback_prefix",
+            false,
+            &StringSchema::new("Loopback IP prefix")
+                .format(&CIDR_FORMAT)
+                .schema()
+        ),
+    ]),
+);
+
+#[sortable]
+const INTERFACE_SCHEMA: Schema = ObjectSchema::new(
+    "interface",
+    &sorted!([
+        (
+            "name",
+            false,
+            &StringSchema::new("Interface name")
+                .min_length(1)
+                .max_length(15)
+                .schema(),
+        ),
+        (
+            "ip",
+            true,
+            &StringSchema::new("Interface IPv4 address")
+                .format(&CIDR_V4_FORMAT)
+                .schema()
+        ),
+        (
+            "ipv6",
+            true,
+            &StringSchema::new("Interface IPv6 address")
+                .format(&CIDR_V6_FORMAT)
+                .schema()
+        ),
+        (
+            "passive",
+            true,
+            &BooleanSchema::new("OpenFabric passive mode for this interface").schema(),
+        ),
+        (
+            "hello_interval",
+            true,
+            &IntegerSchema::new("OpenFabric Hello interval in seconds")
+                .minimum(1)
+                .maximum(600)
+                .schema(),
+        ),
+        (
+            "csnp_interval",
+            true,
+            &IntegerSchema::new("OpenFabric csnp interval in seconds")
+                .minimum(1)
+                .maximum(600)
+                .schema()
+        ),
+        (
+            "hello_multiplier",
+            true,
+            &IntegerSchema::new("OpenFabric multiplier for Hello holding time")
+                .minimum(2)
+                .maximum(100)
+                .schema()
+        ),
+    ]),
+)
+.schema();
+
+#[sortable]
+const NODE_SCHEMA: ObjectSchema = ObjectSchema::new(
+    "node schema",
+    &sorted!([
+        (
+            "id",
+            false,
+            &StringSchema::new("NodeId containing the fabric_id and hostname").schema(),
+        ),
+        ("fabric_id", false, &StringSchema::new("FabricId").schema()),
+        ("node_id", false, &StringSchema::new("NodeId").schema()),
+        (
+            "interfaces",
+            true,
+            &ArraySchema::new(
+                "OpenFabric name",
+                &StringSchema::new("OpenFabric Interface")
+                    .format(&ApiStringFormat::PropertyString(&INTERFACE_SCHEMA))
+                    .schema(),
+            )
+            .schema(),
+        ),
+        (
+            "router_id",
+            false,
+            &StringSchema::new("OpenFabric router-id")
+                .min_length(3)
+                .schema(),
+        ),
+    ]),
+);
+
+const ID_SCHEMA: Schema = StringSchema::new("id").min_length(2).schema();
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct FabricSection {
+    pub fabric_id: FabricId,
+    #[serde(rename = "type")]
+    pub ty: SectionType,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub hello_interval: Option<HelloInterval>,
+    pub loopback_prefix: Cidr,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct NodeSection {
+    pub id: NodeId,
+    pub fabric_id: FabricId,
+    pub node_id: Hostname,
+    #[serde(rename = "type")]
+    pub ty: SectionType,
+    pub router_id: IpAddr,
+    #[serde(default)]
+    pub interfaces: Vec<PropertyString<InterfaceProperties>>,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct InterfaceProperties {
+    pub name: String,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    #[serde(default, with = "string_as_bool")]
+    pub passive: Option<bool>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub hello_interval: Option<HelloInterval>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub csnp_interval: Option<CsnpInterval>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub hello_multiplier: Option<HelloMultiplier>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub ip: Option<Ipv4Cidr>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub ipv6: Option<Ipv6Cidr>,
+}
+
+impl InterfaceProperties {
+    pub fn passive(&self) -> Option<bool> {
+        self.passive
+    }
+}
+
+impl ApiType for InterfaceProperties {
+    const API_SCHEMA: Schema = INTERFACE_SCHEMA;
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+#[serde(untagged)]
+pub enum OpenFabricSectionConfig {
+    Fabric(FabricSection),
+    Node(NodeSection),
+}
+
+impl ApiSectionDataEntry for OpenFabricSectionConfig {
+    const INTERNALLY_TAGGED: Option<&'static str> = Some("type");
+
+    fn section_config() -> &'static SectionConfig {
+        static SC: OnceLock<SectionConfig> = OnceLock::new();
+
+        SC.get_or_init(|| {
+            let mut config = SectionConfig::new(&ID_SCHEMA);
+
+            let fabric_plugin = SectionConfigPlugin::new(
+                "fabric".to_string(),
+                Some("fabric_id".to_string()),
+                &FABRIC_SCHEMA,
+            );
+            config.register_plugin(fabric_plugin);
+
+            let node_plugin =
+                SectionConfigPlugin::new("node".to_string(), Some("id".to_string()), &NODE_SCHEMA);
+            config.register_plugin(node_plugin);
+
+            config
+        })
+    }
+
+    fn section_type(&self) -> &'static str {
+        match self {
+            Self::Node(_) => "node",
+            Self::Fabric(_) => "fabric",
+        }
+    }
+}
-- 
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 v2 11/15] ve-config: add ospf section-config
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (10 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 10/15] ve-config: add openfabric section-config Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 12/15] ve-config: add FRR conversion helpers for openfabric and ospf Gabriel Goller
                   ` (45 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

This is the main configuration for OSPF. It is used to parse the section
config file and is returned from the api.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 proxmox-ve-config/src/sdn/fabric/ospf/mod.rs | 219 +++++++++++++++++++
 1 file changed, 219 insertions(+)
 create mode 100644 proxmox-ve-config/src/sdn/fabric/ospf/mod.rs

diff --git a/proxmox-ve-config/src/sdn/fabric/ospf/mod.rs b/proxmox-ve-config/src/sdn/fabric/ospf/mod.rs
new file mode 100644
index 000000000000..ece251645ee8
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/ospf/mod.rs
@@ -0,0 +1,219 @@
+#[cfg(feature = "frr")]
+pub mod frr;
+
+use proxmox_network_types::debian::Hostname;
+use proxmox_network_types::ip_address::Ipv4Cidr;
+use proxmox_schema::property_string::PropertyString;
+
+use proxmox_schema::ObjectSchema;
+use proxmox_schema::{ApiStringFormat, ApiType, ArraySchema, BooleanSchema, Schema, StringSchema};
+use proxmox_section_config::{typed::ApiSectionDataEntry, SectionConfig, SectionConfigPlugin};
+use proxmox_sortable_macro::sortable;
+use serde::{Deserialize, Serialize};
+use serde_with::{DeserializeFromStr, SerializeDisplay};
+use std::fmt::Display;
+use std::net::Ipv4Addr;
+use std::str::FromStr;
+use std::sync::OnceLock;
+use thiserror::Error;
+
+use crate::sdn::fabric::{FabricId, NodeId, SectionType};
+
+#[sortable]
+const FABRIC_SCHEMA: ObjectSchema = ObjectSchema::new(
+    "fabric schema",
+    &sorted!([
+        (
+            "area",
+            false,
+            &StringSchema::new("Area identifier").min_length(1).schema()
+        ),
+        ("fabric_id", false, &StringSchema::new("Fabric ID").schema()),
+        (
+            "loopback_prefix",
+            false,
+            &StringSchema::new("Loopback IP prefix")
+                .min_length(1)
+                .schema()
+        ),
+    ]),
+);
+
+#[sortable]
+const INTERFACE_SCHEMA: Schema = ObjectSchema::new(
+    "interface",
+    &sorted!([
+        (
+            "name",
+            false,
+            &StringSchema::new("Interface name")
+                .min_length(1)
+                .max_length(15)
+                .schema(),
+        ),
+        (
+            "passive",
+            true,
+            &BooleanSchema::new("passive interface").schema(),
+        ),
+        (
+            "unnumbered",
+            true,
+            &BooleanSchema::new("unnumbered interface").schema(),
+        ),
+        (
+            "ip",
+            true,
+            &StringSchema::new("Interface IPv4 address").schema()
+        ),
+    ]),
+)
+.schema();
+
+#[sortable]
+const NODE_SCHEMA: ObjectSchema = ObjectSchema::new(
+    "node schema",
+    &sorted!([
+        (
+            "id",
+            false,
+            &StringSchema::new("NodeId which contains fabric id and node hostname").schema()
+        ),
+        ("fabric_id", false, &StringSchema::new("Fabric ID").schema()),
+        ("node_id", false, &StringSchema::new("Node ID").schema()),
+        (
+            "interfaces",
+            true,
+            &ArraySchema::new(
+                "OSPF name",
+                &StringSchema::new("OSPF Interface")
+                    .format(&ApiStringFormat::PropertyString(&INTERFACE_SCHEMA))
+                    .schema(),
+            )
+            .schema(),
+        ),
+        (
+            "router_id",
+            false,
+            &StringSchema::new("OSPF router id").min_length(3).schema(),
+        ),
+    ]),
+);
+
+const ID_SCHEMA: Schema = StringSchema::new("id").min_length(2).schema();
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct InterfaceProperties {
+    pub name: String,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub passive: Option<bool>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub unnumbered: Option<bool>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub ip: Option<Ipv4Cidr>,
+}
+
+impl ApiType for InterfaceProperties {
+    const API_SCHEMA: Schema = INTERFACE_SCHEMA;
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub struct NodeSection {
+    pub id: NodeId,
+    pub fabric_id: FabricId,
+    pub node_id: Hostname,
+    #[serde(rename = "type")]
+    pub ty: SectionType,
+    pub router_id: Ipv4Addr,
+    #[serde(default)]
+    pub interfaces: Vec<PropertyString<InterfaceProperties>>,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct FabricSection {
+    pub fabric_id: FabricId,
+    #[serde(rename = "type")]
+    pub ty: SectionType,
+    pub area: Area,
+    pub loopback_prefix: Ipv4Cidr,
+}
+
+#[derive(Error, Debug)]
+pub enum AreaParsingError {
+    #[error("Invalid area identifier. Area must be a number or a ipv4 address.")]
+    InvalidArea,
+}
+
+#[derive(
+    Debug, DeserializeFromStr, SerializeDisplay, Hash, PartialEq, Eq, PartialOrd, Ord, Clone,
+)]
+pub struct Area(String);
+
+impl Area {
+    pub fn new(area: String) -> Result<Area, AreaParsingError> {
+        if area.parse::<u32>().is_ok() || area.parse::<Ipv4Addr>().is_ok() {
+            Ok(Self(area))
+        } else {
+            Err(AreaParsingError::InvalidArea)
+        }
+    }
+}
+
+impl FromStr for Area {
+    type Err = AreaParsingError;
+
+    fn from_str(value: &str) -> Result<Self, Self::Err> {
+        Area::new(value.to_owned())
+    }
+}
+
+impl AsRef<str> for Area {
+    fn as_ref(&self) -> &str {
+        &self.0
+    }
+}
+
+impl Display for Area {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+#[serde(untagged)]
+pub enum OspfSectionConfig {
+    Fabric(FabricSection),
+    Node(NodeSection),
+}
+
+impl ApiSectionDataEntry for OspfSectionConfig {
+    const INTERNALLY_TAGGED: Option<&'static str> = Some("type");
+
+    fn section_config() -> &'static SectionConfig {
+        static SC: OnceLock<SectionConfig> = OnceLock::new();
+
+        SC.get_or_init(|| {
+            let mut config = SectionConfig::new(&ID_SCHEMA);
+
+            let fabric_plugin = SectionConfigPlugin::new(
+                "fabric".to_string(),
+                Some("fabric_id".to_string()),
+                &FABRIC_SCHEMA,
+            );
+            config.register_plugin(fabric_plugin);
+
+            let node_plugin =
+                SectionConfigPlugin::new("node".to_string(), Some("id".to_string()), &NODE_SCHEMA);
+            config.register_plugin(node_plugin);
+
+            config
+        })
+    }
+
+    fn section_type(&self) -> &'static str {
+        match self {
+            Self::Node(_) => "node",
+            Self::Fabric(_) => "fabric",
+        }
+    }
+}
-- 
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 v2 12/15] ve-config: add FRR conversion helpers for openfabric and ospf
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (11 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 11/15] ve-config: add ospf section-config Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 13/15] ve-config: add validation for section-config Gabriel Goller
                   ` (44 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

Add conversion helpers for FRR interfaces. We can't put these in eg.
`TryInto` implementations, as we need a tupel and tupels are foreign
types. Create a simple conversion function that converts the OpenFabric
and OSPF interfaces to FRR interfaces.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 .../src/sdn/fabric/openfabric/frr.rs          | 24 +++++++++++++++++++
 proxmox-ve-config/src/sdn/fabric/ospf/frr.rs  | 21 ++++++++++++++++
 2 files changed, 45 insertions(+)
 create mode 100644 proxmox-ve-config/src/sdn/fabric/openfabric/frr.rs
 create mode 100644 proxmox-ve-config/src/sdn/fabric/ospf/frr.rs

diff --git a/proxmox-ve-config/src/sdn/fabric/openfabric/frr.rs b/proxmox-ve-config/src/sdn/fabric/openfabric/frr.rs
new file mode 100644
index 000000000000..682fe62ab72f
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/openfabric/frr.rs
@@ -0,0 +1,24 @@
+use proxmox_frr::{
+    openfabric::{OpenFabricInterface, OpenFabricInterfaceError},
+    FrrWord,
+};
+
+use super::{FabricId, InterfaceProperties};
+
+impl InterfaceProperties {
+    pub fn to_frr_interface(
+        &self,
+        fabric_id: &FabricId,
+        is_ipv6: bool,
+    ) -> Result<OpenFabricInterface, OpenFabricInterfaceError> {
+        let frr_word = FrrWord::new(fabric_id.to_string())?;
+        Ok(OpenFabricInterface {
+            fabric_id: frr_word.into(),
+            passive: self.passive(),
+            hello_interval: self.hello_interval,
+            csnp_interval: self.csnp_interval,
+            hello_multiplier: self.hello_multiplier,
+            is_ipv6,
+        })
+    }
+}
diff --git a/proxmox-ve-config/src/sdn/fabric/ospf/frr.rs b/proxmox-ve-config/src/sdn/fabric/ospf/frr.rs
new file mode 100644
index 000000000000..1a09dd9b5a9b
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/ospf/frr.rs
@@ -0,0 +1,21 @@
+use proxmox_frr::{
+    ospf::{NetworkType, OspfInterface, OspfInterfaceError},
+    FrrWord,
+};
+
+use super::{Area, InterfaceProperties};
+
+impl InterfaceProperties {
+    pub fn to_frr_interface(&self, area: &Area) -> Result<OspfInterface, OspfInterfaceError> {
+        let frr_word = FrrWord::new(area.to_string())?;
+        Ok(OspfInterface {
+            area: frr_word.try_into()?,
+            passive: self.passive,
+            network_type: if let Some(true) = self.unnumbered {
+                Some(NetworkType::PointToPoint)
+            } else {
+                None
+            },
+        })
+    }
+}
-- 
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 v2 13/15] ve-config: add validation for section-config
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (12 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 12/15] ve-config: add FRR conversion helpers for openfabric and ospf Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 14/15] ve-config: add section-config to frr types conversion Gabriel Goller
                   ` (43 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

Our section-config is nested 3 times (fabric -> node -> interfaces), but
as only one indentation level (two with propertyStrings) are possible in
section-config configuration files, we need to add some validation to
ensure that the config is valid. In the future, more stuff to be
validated can be added here, but currently we check:

 * if the router-id is unique
 * if the node refers to a existing fabric
 * if the router-ids are in the specified loopback prefix

Our Section-Config is structured like this:

fabric: test
    fabric-option: this

node: test_pve0
    node-option: that

The key to the node section is called the `NodeId` and consist of two
parts: `test`, which is the fabric, and `pve0`, which is the nodename.
The validation checks if the `test` fabric exists.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/src/sdn/fabric/mod.rs       | 33 +++++++++
 .../src/sdn/fabric/openfabric/mod.rs          |  1 +
 .../src/sdn/fabric/openfabric/validation.rs   | 58 ++++++++++++++++
 proxmox-ve-config/src/sdn/fabric/ospf/mod.rs  |  1 +
 .../src/sdn/fabric/ospf/validation.rs         | 68 +++++++++++++++++++
 proxmox-ve-config/src/sdn/mod.rs              |  1 +
 6 files changed, 162 insertions(+)
 create mode 100644 proxmox-ve-config/src/sdn/fabric/openfabric/validation.rs
 create mode 100644 proxmox-ve-config/src/sdn/fabric/ospf/validation.rs

diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs
index 5b24a3be8c17..45795b0e51b0 100644
--- a/proxmox-ve-config/src/sdn/fabric/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/mod.rs
@@ -109,3 +109,36 @@ impl std::str::FromStr for SectionType {
         })
     }
 }
+
+#[derive(Debug, Clone)]
+pub struct Valid<T>(SectionConfigData<T>);
+
+impl<T> Deref for Valid<T> {
+    type Target = SectionConfigData<T>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+pub trait Validate<T> {
+    fn validate(data: SectionConfigData<T>) -> Result<Valid<T>, anyhow::Error>;
+    fn validate_as_ref(data: &SectionConfigData<T>) -> Result<(), anyhow::Error>;
+}
+
+impl<T> Valid<T> {
+    pub fn into_inner(self) -> SectionConfigData<T> {
+        self.0
+    }
+}
+
+impl<T> Valid<T>
+where
+    T: ApiSectionDataEntry + DeserializeOwned + Validate<T>,
+{
+    pub fn parse_section_config(filename: &str, data: &str) -> Result<Valid<T>, anyhow::Error> {
+        let config = T::parse_section_config(filename, data)?;
+        T::validate(config)
+    }
+}
+
diff --git a/proxmox-ve-config/src/sdn/fabric/openfabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/openfabric/mod.rs
index 6e7f4e947a0e..2aca197e095b 100644
--- a/proxmox-ve-config/src/sdn/fabric/openfabric/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/openfabric/mod.rs
@@ -1,5 +1,6 @@
 #[cfg(feature = "frr")]
 pub mod frr;
+pub mod validation;
 
 use proxmox_network_types::{
     debian::Hostname,
diff --git a/proxmox-ve-config/src/sdn/fabric/openfabric/validation.rs b/proxmox-ve-config/src/sdn/fabric/openfabric/validation.rs
new file mode 100644
index 000000000000..bf0c02620e63
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/openfabric/validation.rs
@@ -0,0 +1,58 @@
+use anyhow::{anyhow, bail};
+use std::collections::{HashMap, HashSet};
+
+use proxmox_section_config::typed::SectionConfigData;
+
+use crate::sdn::fabric::{Valid, Validate};
+
+use super::OpenFabricSectionConfig;
+
+impl Validate<OpenFabricSectionConfig> for OpenFabricSectionConfig {
+    /// This function will validate the SectionConfigData<T> and return a Valid<SectionConfigData<T>>
+    /// The validation checks if the every node is part of an existing fabric. This is necessary as
+    /// with the current SectionConfigData format, we don't get this guarantee.
+    fn validate(
+        data: SectionConfigData<OpenFabricSectionConfig>,
+    ) -> Result<Valid<OpenFabricSectionConfig>, anyhow::Error> {
+        Self::validate_as_ref(&data)?;
+        Ok(Valid(data))
+    }
+
+    fn validate_as_ref(
+        data: &SectionConfigData<OpenFabricSectionConfig>,
+    ) -> Result<(), anyhow::Error> {
+        let mut fabrics = HashMap::new();
+        let mut nodes = Vec::new();
+
+        for (_, section) in data {
+            match section {
+                OpenFabricSectionConfig::Node(node) => {
+                    nodes.push(node);
+                }
+                OpenFabricSectionConfig::Fabric(fabric) => {
+                    fabrics.insert(&fabric.fabric_id, fabric);
+                }
+            }
+        }
+
+        let mut router_ids = HashSet::new();
+
+        for node in nodes {
+            let fabric = fabrics
+                .get(&node.fabric_id)
+                .ok_or_else(|| anyhow!("Validation Error: Missing fabric configuration"))?;
+
+            if !router_ids.insert(node.router_id) {
+                bail!("Validation Error: Duplicate loopback ip");
+            }
+
+            if !fabric.loopback_prefix.contains_address(&node.router_id) {
+                bail!(
+                    "Validation Error: Loopback IP of node is not contained in Loopback IP prefix"
+                );
+            }
+        }
+
+        Ok(())
+    }
+}
diff --git a/proxmox-ve-config/src/sdn/fabric/ospf/mod.rs b/proxmox-ve-config/src/sdn/fabric/ospf/mod.rs
index ece251645ee8..b1bec62cfa1c 100644
--- a/proxmox-ve-config/src/sdn/fabric/ospf/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/ospf/mod.rs
@@ -1,5 +1,6 @@
 #[cfg(feature = "frr")]
 pub mod frr;
+pub mod validation;
 
 use proxmox_network_types::debian::Hostname;
 use proxmox_network_types::ip_address::Ipv4Cidr;
diff --git a/proxmox-ve-config/src/sdn/fabric/ospf/validation.rs b/proxmox-ve-config/src/sdn/fabric/ospf/validation.rs
new file mode 100644
index 000000000000..539515485dac
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/ospf/validation.rs
@@ -0,0 +1,68 @@
+use anyhow::{anyhow, bail};
+use std::collections::{HashMap, HashSet};
+
+use proxmox_section_config::typed::SectionConfigData;
+
+use crate::sdn::fabric::{Valid, Validate};
+
+use super::OspfSectionConfig;
+
+impl Validate<OspfSectionConfig> for OspfSectionConfig {
+    /// This function will validate the SectionConfigData<T> and return a Valid<SectionConfigData<T>>
+    /// The validation checks if the every node is part of an existing fabric. This is necessary as
+    /// with the current SectionConfigData format, we don't get this guarantee.
+    fn validate(
+        data: SectionConfigData<OspfSectionConfig>,
+    ) -> Result<Valid<OspfSectionConfig>, anyhow::Error> {
+        Self::validate_as_ref(&data)?;
+        Ok(Valid(data))
+    }
+
+    fn validate_as_ref(data: &SectionConfigData<OspfSectionConfig>) -> Result<(), anyhow::Error> {
+        let mut fabrics = HashMap::new();
+        let mut nodes = Vec::new();
+
+        for (_, section) in data {
+            match section {
+                OspfSectionConfig::Node(node) => {
+                    nodes.push(node);
+                }
+                OspfSectionConfig::Fabric(fabric) => {
+                    fabrics.insert(&fabric.fabric_id, fabric);
+                }
+            }
+        }
+
+        let mut router_ids = HashSet::new();
+
+        for node in nodes {
+            let fabric = fabrics
+                .get(&node.fabric_id)
+                .ok_or_else(|| anyhow!("Validation Error: Missing fabric configuration"))?;
+
+            if !router_ids.insert(node.router_id) {
+                bail!("Validation Error: Duplicate loopback ip");
+            }
+
+            if !fabric.loopback_prefix.contains_address(&node.router_id) {
+                bail!(
+                    "Validation Error: Loopback IP of node is not contained in Loopback IP prefix"
+                );
+            }
+
+            for interface in &node.interfaces {
+                match (interface.ip.is_some(), interface.unnumbered) {
+                    (true, Some(true)) => {
+                        bail!("Validation Error: Interface cannot be both unnumbered and have an IP address");
+                    }
+                    (false, None | Some(false)) => {
+                        bail!("Validation Error: Interface \"{}\" on node \"{}\" must either be unnumbered or have an IP address", interface.name, node.node_id);
+                    }
+                    _ => (),
+                }
+            }
+        }
+
+        Ok(())
+    }
+}
diff --git a/proxmox-ve-config/src/sdn/mod.rs b/proxmox-ve-config/src/sdn/mod.rs
index cde6fed88f26..7a46db3d85bb 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 v2 14/15] ve-config: add section-config to frr types conversion
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (13 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 13/15] ve-config: add validation for section-config Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 15/15] ve-config: add integrations tests Gabriel Goller
                   ` (42 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

Add a FabricConfig builder which iterates through nodes and generates
the frr config for the specified current_node. This part also
distributes the fabric options on all the interfaces – e.g. the
hello-interval option on the fabric will be added to all interfaces
here.

We mainly need to add these objects to FRR:

* interfaces
    We simply iterate through all configured interfaces and add them FRR
    with a short config line telling the daemon to enable
    openfabric/ospf on this interface.

* routers
    The tell the FRR daemon to initiate the openfabric/ospf daemon on
    every node.

* access-lists
    We throw all the router-ips of all the other nodes in the same
    fabric in access-list. This way we can simply use a route-map to
    match on it.

* route-maps
    We add a route-map to every fabric so that we rewrite the source
    address to the current router-ip which is on the local
    dummy_interface.

* ip-protocol statements
    These add the route-map to the protocol and all the routes from the
    protocol are going through the route-map.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 proxmox-ve-config/Cargo.toml            |   5 +
 proxmox-ve-config/debian/control        |  23 +-
 proxmox-ve-config/src/sdn/fabric/mod.rs | 427 ++++++++++++++++++++++++
 3 files changed, 453 insertions(+), 2 deletions(-)

diff --git a/proxmox-ve-config/Cargo.toml b/proxmox-ve-config/Cargo.toml
index f58c6e2d0b8c..70c25312ac6d 100644
--- a/proxmox-ve-config/Cargo.toml
+++ b/proxmox-ve-config/Cargo.toml
@@ -11,12 +11,14 @@ log = "0.4"
 anyhow = "1"
 nix = "0.26"
 thiserror = { workspace = true }
+tracing = "0.1"
 
 serde = { workspace = true, features = [ "derive" ] }
 serde_json = "1"
 serde_plain = "1"
 serde_with = { workspace = true }
 
+proxmox-frr = { optional = true, workspace = true }
 proxmox-network-types = { workspace = true }
 proxmox-sdn-types = { workspace = true }
 proxmox-schema = { version = "4", features = [ "api-types" ] }
@@ -24,3 +26,6 @@ proxmox-section-config = { version = "2.1.2" }
 proxmox-serde = { version = "0.1.2" }
 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 0da241fed775..d0940b2c8ac5 100644
--- a/proxmox-ve-config/debian/control
+++ b/proxmox-ve-config/debian/control
@@ -22,7 +22,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 <!nocheck>
 Maintainer: Proxmox Support Team <support@proxmox.com>
 Standards-Version: 4.7.0
 Vcs-Git: git://git.proxmox.com/git/proxmox-ve-rs.git
@@ -51,7 +52,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
+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}),
@@ -62,3 +66,18 @@ Provides:
  librust-proxmox-ve-config-0.2.2+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.2+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/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs
index 45795b0e51b0..b3580ec4c7d0 100644
--- a/proxmox-ve-config/src/sdn/fabric/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/mod.rs
@@ -1,9 +1,12 @@
 pub mod openfabric;
 pub mod ospf;
 
+use openfabric::OpenFabricSectionConfig;
+use ospf::OspfSectionConfig;
 use proxmox_network_types::debian::Hostname;
 use proxmox_section_config::typed::ApiSectionDataEntry;
 use proxmox_section_config::typed::SectionConfigData;
+use thiserror::Error;
 
 use std::ops::Deref;
 
@@ -110,6 +113,22 @@ impl std::str::FromStr for SectionType {
     }
 }
 
+#[cfg(feature = "frr")]
+use {
+    anyhow::anyhow,
+    proxmox_frr::{
+        ospf::Area,
+        route_map::{
+            AccessAction, AccessList, AccessListName, AccessListRule, ProtocolRouteMap,
+            ProtocolType, RouteMap, RouteMapMatch, RouteMapName, RouteMapSet,
+        },
+        FrrConfig, FrrWord, Interface, InterfaceName, Router, RouterName,
+    },
+    proxmox_sdn_types::net::Net,
+    std::collections::{BTreeMap, HashMap},
+    std::net::{IpAddr, Ipv4Addr},
+};
+
 #[derive(Debug, Clone)]
 pub struct Valid<T>(SectionConfigData<T>);
 
@@ -142,3 +161,411 @@ where
     }
 }
 
+#[derive(Error, Debug)]
+pub enum ConfigError {
+    #[error("node id has invalid format")]
+    InvalidNodeId,
+}
+
+#[derive(Default, Clone)]
+pub struct FabricConfig {
+    openfabric: Option<Valid<OpenFabricSectionConfig>>,
+    ospf: Option<Valid<OspfSectionConfig>>,
+}
+
+impl FabricConfig {
+    pub fn new(raw_openfabric: &str, raw_ospf: &str) -> Result<Self, anyhow::Error> {
+        let openfabric = <Valid<OpenFabricSectionConfig>>::parse_section_config(
+            "openfabric.cfg",
+            raw_openfabric,
+        )?;
+        let ospf = <Valid<OspfSectionConfig>>::parse_section_config("ospf.cfg", raw_ospf)?;
+
+        Ok(Self {
+            openfabric: Some(openfabric),
+            ospf: Some(ospf),
+        })
+    }
+
+    pub fn openfabric(&self) -> &Option<Valid<OpenFabricSectionConfig>> {
+        &self.openfabric
+    }
+    pub fn ospf(&self) -> &Option<Valid<OspfSectionConfig>> {
+        &self.ospf
+    }
+
+    pub fn with_openfabric(config: Valid<OpenFabricSectionConfig>) -> FabricConfig {
+        Self {
+            openfabric: Some(config),
+            ospf: None,
+        }
+    }
+
+    pub fn with_ospf(config: Valid<OspfSectionConfig>) -> FabricConfig {
+        Self {
+            ospf: Some(config),
+            openfabric: None,
+        }
+    }
+}
+
+pub trait FromSectionConfig
+where
+    Self: Sized + TryFrom<SectionConfigData<Self::Section>>,
+    <Self as TryFrom<SectionConfigData<Self::Section>>>::Error: std::fmt::Debug,
+{
+    type Section: ApiSectionDataEntry + DeserializeOwned;
+
+    fn from_section_config(raw: &str) -> Result<Self, anyhow::Error> {
+        let section_config_data = Self::Section::section_config()
+            .parse(Self::filename(), raw)?
+            .try_into()?;
+
+        let output = Self::try_from(section_config_data).unwrap();
+        Ok(output)
+    }
+
+    fn filename() -> String;
+}
+
+/// Builder that helps building the FrrConfig.
+#[derive(Default)]
+#[cfg(feature = "frr")]
+pub struct FrrConfigBuilder {
+    fabrics: FabricConfig,
+}
+
+#[cfg(feature = "frr")]
+impl FrrConfigBuilder {
+    /// Add fabrics to the builder
+    pub fn add_fabrics(mut self, fabric: 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: Hostname) -> Result<FrrConfig, anyhow::Error> {
+        let mut router: BTreeMap<RouterName, Router> = BTreeMap::new();
+        let mut interfaces: BTreeMap<InterfaceName, Interface> = BTreeMap::new();
+        let mut access_lists: BTreeMap<AccessListName, AccessList> = BTreeMap::new();
+        let mut routemaps: Vec<RouteMap> = Vec::new();
+        let mut protocol_routemaps: Vec<ProtocolRouteMap> = Vec::new();
+
+        if let Some(openfabric) = self.fabrics.openfabric {
+            let mut fabrics = HashMap::new();
+            let mut local_configuration = Vec::new();
+
+            for (_, section) in (&*openfabric).into_iter() {
+                match section {
+                    OpenFabricSectionConfig::Fabric(fabric) => {
+                        fabrics.insert(fabric.fabric_id.clone(), fabric);
+                    }
+                    OpenFabricSectionConfig::Node(node) => {
+                        if node.node_id == current_node {
+                            local_configuration.push(node);
+                        }
+                    }
+                }
+            }
+
+            let mut routemap_seq = 100;
+            let mut current_net: Option<Net> = None;
+
+            for node in local_configuration {
+                // if no interfaces are configured, don't generate any config
+                if node.interfaces.is_empty() {
+                    break;
+                }
+
+                let fabric = fabrics
+                    .get(&node.fabric_id)
+                    .ok_or_else(|| anyhow!("could not find fabric: {}", node.fabric_id))?;
+
+                let net = current_net.get_or_insert(node.router_id.into());
+                let (router_name, router_item) =
+                    Self::build_openfabric_router(&node.fabric_id, net)?;
+                router.insert(router_name, router_item);
+
+                let (interface, interface_name) =
+                    Self::build_openfabric_dummy_interface(&node.fabric_id, node.router_id)?;
+
+                if interfaces.insert(interface_name, interface).is_some() {
+                    tracing::error!(
+                        "An interface with the same name as the dummy interface exists"
+                    );
+                }
+
+                for interface in node.interfaces.iter() {
+                    let (interface, interface_name) = Self::build_openfabric_interface(
+                        &node.fabric_id,
+                        interface,
+                        fabric,
+                        node.router_id,
+                    )?;
+
+                    if interfaces.insert(interface_name, interface).is_some() {
+                        tracing::warn!("An interface cannot be in multiple openfabric fabrics");
+                    }
+                }
+
+                let access_list_name =
+                    AccessListName::new(format!("openfabric_{}_ips", node.fabric_id));
+
+                let rule = AccessListRule {
+                    action: AccessAction::Permit,
+                    network: fabric.loopback_prefix,
+                    seq: None,
+                };
+
+                access_lists
+                    .entry(access_list_name.clone())
+                    .and_modify(|l| l.rules.push(rule.clone()))
+                    .or_insert(AccessList {
+                        name: access_list_name,
+                        rules: vec![rule],
+                    });
+
+                let routemap = Self::build_openfabric_dummy_routemap(
+                    &node.fabric_id,
+                    node.router_id,
+                    routemap_seq,
+                )?;
+
+                routemap_seq += 10;
+
+                routemaps.push(routemap);
+            }
+
+            if !routemaps.is_empty() {
+                let protocol_routemap = ProtocolRouteMap {
+                    protocol: ProtocolType::OpenFabric,
+                    routemap_name: RouteMapName::new("openfabric".to_owned()),
+                };
+
+                protocol_routemaps.push(protocol_routemap);
+            }
+        }
+
+        if let Some(ospf) = self.fabrics.ospf {
+            let mut fabrics = HashMap::new();
+            let mut local_configuration = Vec::new();
+
+            for (_, section) in (&*ospf).into_iter() {
+                match section {
+                    OspfSectionConfig::Fabric(fabric) => {
+                        fabrics.insert(fabric.fabric_id.clone(), fabric);
+                    }
+                    OspfSectionConfig::Node(node) => {
+                        if node.node_id == current_node {
+                            local_configuration.push(node);
+                        }
+                    }
+                }
+            }
+
+            let mut routemap_seq = 100;
+            let mut current_router_id: Option<Ipv4Addr> = None;
+
+            for node in local_configuration {
+                // if no interfaces are configured, don't generate any config
+                if node.interfaces.is_empty() {
+                    break;
+                }
+
+                let fabric = fabrics
+                    .get(&node.fabric_id)
+                    .ok_or_else(|| anyhow!("could not find fabric: {}", node.fabric_id))?;
+
+                let router_id = current_router_id.get_or_insert(node.router_id);
+                let (router_name, router_item) =
+                    Self::build_ospf_router(&fabric.area, node, *router_id)?;
+                router.insert(router_name, router_item);
+
+                // Add dummy interface
+                let (interface, interface_name) =
+                    Self::build_ospf_dummy_interface(&fabric.fabric_id, &fabric.area)?;
+
+                if interfaces.insert(interface_name, interface).is_some() {
+                    tracing::error!(
+                        "An interface with the same name as the dummy interface exists"
+                    );
+                }
+
+                for interface in node.interfaces.iter() {
+                    let (interface, interface_name) =
+                        Self::build_ospf_interface(&fabric.area, interface)?;
+
+                    if interfaces.insert(interface_name, interface).is_some() {
+                        tracing::warn!("An interface cannot be in multiple openfabric fabrics");
+                    }
+                }
+
+                let access_list_name = AccessListName::new(format!("ospf_{}_ips", node.fabric_id));
+
+                let rule = AccessListRule {
+                    action: AccessAction::Permit,
+                    network: fabric.loopback_prefix.into(),
+                    seq: None,
+                };
+
+                access_lists
+                    .entry(access_list_name.clone())
+                    .and_modify(|l| l.rules.push(rule.clone()))
+                    .or_insert(AccessList {
+                        name: access_list_name,
+                        rules: vec![rule],
+                    });
+
+                let routemap = Self::build_ospf_dummy_routemap(
+                    &fabric.fabric_id,
+                    node.router_id,
+                    routemap_seq,
+                )?;
+                routemap_seq += 10;
+                routemaps.push(routemap);
+            }
+
+            if !routemaps.is_empty() {
+                let protocol_routemap = ProtocolRouteMap {
+                    protocol: ProtocolType::Ospf,
+                    routemap_name: RouteMapName::new("ospf".to_owned()),
+                };
+
+                protocol_routemaps.push(protocol_routemap);
+            }
+        }
+
+        Ok(FrrConfig {
+            router,
+            interfaces,
+            access_lists,
+            routemaps,
+            protocol_routemaps,
+        })
+    }
+
+    fn build_ospf_router(
+        area: &ospf::Area,
+        _node_config: &ospf::NodeSection,
+        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 frr_word_id = FrrWord::new(area.to_string())?;
+        let router_name = RouterName::Ospf(proxmox_frr::ospf::OspfRouterName::from(Area::new(
+            frr_word_id,
+        )?));
+        Ok((router_name, router_item))
+    }
+
+    fn build_openfabric_router(
+        fabric_id: &FabricId,
+        net: &Net,
+    ) -> Result<(RouterName, Router), anyhow::Error> {
+        let ofr = proxmox_frr::openfabric::OpenFabricRouter { net: net.clone() };
+        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))
+    }
+
+    fn build_ospf_interface(
+        area: &ospf::Area,
+        interface: &ospf::InterfaceProperties,
+    ) -> Result<(Interface, InterfaceName), anyhow::Error> {
+        let frr_interface: proxmox_frr::ospf::OspfInterface = interface.to_frr_interface(area)?;
+
+        let interface_name = InterfaceName::Ospf(interface.name.parse()?);
+        Ok((frr_interface.into(), interface_name))
+    }
+
+    fn build_ospf_dummy_interface(
+        fabric_id: &FabricId,
+        area: &ospf::Area,
+    ) -> Result<(Interface, InterfaceName), anyhow::Error> {
+        let frr_word = FrrWord::new(area.to_string())?;
+        let frr_interface = proxmox_frr::ospf::OspfInterface {
+            area: frr_word.try_into()?,
+            passive: Some(true),
+            network_type: None,
+        };
+        let interface_name = InterfaceName::OpenFabric(format!("dummy_{}", fabric_id).parse()?);
+        Ok((frr_interface.into(), interface_name))
+    }
+
+    fn build_openfabric_interface(
+        fabric_id: &FabricId,
+        interface: &openfabric::InterfaceProperties,
+        fabric_config: &openfabric::FabricSection,
+        router_id: IpAddr,
+    ) -> Result<(Interface, InterfaceName), anyhow::Error> {
+        let mut frr_interface: proxmox_frr::openfabric::OpenFabricInterface =
+            interface.to_frr_interface(fabric_id, router_id.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))
+    }
+
+    fn build_openfabric_dummy_interface(
+        fabric_id: &FabricId,
+        router_id: IpAddr,
+    ) -> 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_ipv6: router_id.is_ipv6(),
+        };
+        let interface_name = InterfaceName::OpenFabric(format!("dummy_{}", fabric_id).parse()?);
+        Ok((frr_interface.into(), interface_name))
+    }
+
+    fn build_openfabric_dummy_routemap(
+        fabric_id: &FabricId,
+        router_ip: IpAddr,
+        seq: u32,
+    ) -> Result<RouteMap, anyhow::Error> {
+        let routemap_name = RouteMapName::new("openfabric".to_owned());
+        // create route-map
+        let routemap = RouteMap {
+            name: routemap_name.clone(),
+            seq,
+            action: AccessAction::Permit,
+            matches: vec![RouteMapMatch::IpAddress(AccessListName::new(format!(
+                "openfabric_{fabric_id}_ips"
+            )))],
+            sets: vec![RouteMapSet::IpSrc(router_ip)],
+        };
+        Ok(routemap)
+    }
+
+    fn build_ospf_dummy_routemap(
+        fabric_id: &FabricId,
+        router_ip: Ipv4Addr,
+        seq: u32,
+    ) -> Result<RouteMap, anyhow::Error> {
+        let routemap_name = RouteMapName::new("ospf".to_owned());
+        // create route-map
+        let routemap = RouteMap {
+            name: routemap_name.clone(),
+            seq,
+            action: AccessAction::Permit,
+            matches: vec![RouteMapMatch::IpAddress(AccessListName::new(format!(
+                "ospf_{fabric_id}_ips"
+            )))],
+            sets: vec![RouteMapSet::IpSrc(IpAddr::from(router_ip))],
+        };
+
+        Ok(routemap)
+    }
+}
-- 
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 v2 15/15] ve-config: add integrations tests
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (14 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 14/15] ve-config: add section-config to frr types conversion Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-perl-rs v2 1/7] perl-rs: sdn: initial fabric infrastructure Gabriel Goller
                   ` (41 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

Add integration tests for the full cycle from section-config to FRR
config file for both openfabric and ospf.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 proxmox-ve-config/Cargo.toml                  |   3 +
 proxmox-ve-config/debian/control              |   2 +
 .../tests/fabric/cfg/openfabric_default.cfg   |  24 ++++
 .../cfg/openfabric_loopback_prefix_fail.cfg   |  24 ++++
 .../fabric/cfg/openfabric_multi_fabric.cfg    |  33 +++++
 .../cfg/openfabric_verification_fail.cfg      |  16 +++
 .../tests/fabric/cfg/ospf_default.cfg         |  16 +++
 ...f_interface_properties_validation_fail.cfg |  10 ++
 .../fabric/cfg/ospf_loopback_prefix_fail.cfg  |  23 +++
 .../tests/fabric/cfg/ospf_multi_fabric.cfg    |  33 +++++
 .../fabric/cfg/ospf_verification_fail.cfg     |  17 +++
 proxmox-ve-config/tests/fabric/helper.rs      |  43 ++++++
 proxmox-ve-config/tests/fabric/main.rs        | 132 ++++++++++++++++++
 .../fabric__openfabric_default_pve.snap       |  36 +++++
 .../fabric__openfabric_default_pve1.snap      |  32 +++++
 .../fabric__openfabric_multi_fabric_pve1.snap |  48 +++++++
 .../snapshots/fabric__ospf_default_pve.snap   |  31 ++++
 .../snapshots/fabric__ospf_default_pve1.snap  |  27 ++++
 .../fabric__ospf_multi_fabric_pve1.snap       |  48 +++++++
 19 files changed, 598 insertions(+)
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/openfabric_default.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/openfabric_loopback_prefix_fail.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/openfabric_multi_fabric.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/openfabric_verification_fail.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/ospf_default.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/ospf_interface_properties_validation_fail.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/ospf_loopback_prefix_fail.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/ospf_multi_fabric.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/ospf_verification_fail.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_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 70c25312ac6d..74d32df49d9d 100644
--- a/proxmox-ve-config/Cargo.toml
+++ b/proxmox-ve-config/Cargo.toml
@@ -29,3 +29,6 @@ proxmox-sortable-macro = "0.1.3"
 
 [features]
 frr = ["dep:proxmox-frr" ]
+
+[dev-dependencies]
+insta = "1.21"
diff --git a/proxmox-ve-config/debian/control b/proxmox-ve-config/debian/control
index d0940b2c8ac5..d251fda43367 100644
--- a/proxmox-ve-config/debian/control
+++ b/proxmox-ve-config/debian/control
@@ -7,6 +7,7 @@ Build-Depends-Arch: cargo:native <!nocheck>,
  rustc:native <!nocheck>,
  libstd-rust-dev <!nocheck>,
  librust-anyhow-1+default-dev <!nocheck>,
+ librust-insta-1+json-dev (>= 1.21-~~),
  librust-log-0.4+default-dev <!nocheck>,
  librust-nix-0.26+default-dev <!nocheck>,
  librust-proxmox-network-types-0.1+default-dev <!nocheck>,
@@ -37,6 +38,7 @@ Multi-Arch: same
 Depends:
  ${misc:Depends},
  librust-anyhow-1+default-dev,
+ librust-insta-1+json-dev (>= 1.21-~~),
  librust-log-0.4+default-dev,
  librust-nix-0.26+default-dev,
  librust-proxmox-network-types-0.1+default-dev,
diff --git a/proxmox-ve-config/tests/fabric/cfg/openfabric_default.cfg b/proxmox-ve-config/tests/fabric/cfg/openfabric_default.cfg
new file mode 100644
index 000000000000..676862e3ce45
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/openfabric_default.cfg
@@ -0,0 +1,24 @@
+fabric: uwu
+        hello_interval 4
+        loopback_prefix 192.168.2.0/24
+
+node: uwu_pve
+        fabric_id uwu
+        interfaces name=ens20,passive=1,hello_interval=3,hello_multiplier=50
+        interfaces name=ens19,passive=1,csnp_interval=100
+        node_id pve
+        router_id 192.168.2.8
+
+node: uwu_pve1
+        fabric_id uwu
+        interfaces name=ens19
+        interfaces name=ens20
+        node_id pve1
+        router_id 192.168.2.9
+
+node: uwu_pve2
+        fabric_id uwu
+        interfaces name=ens19
+        interfaces name=ens20
+        node_id pve2
+        router_id 192.168.2.10
diff --git a/proxmox-ve-config/tests/fabric/cfg/openfabric_loopback_prefix_fail.cfg b/proxmox-ve-config/tests/fabric/cfg/openfabric_loopback_prefix_fail.cfg
new file mode 100644
index 000000000000..e795c1524f9b
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/openfabric_loopback_prefix_fail.cfg
@@ -0,0 +1,24 @@
+fabric: test
+        hello_interval 4
+        loopback_prefix 192.168.2.0/28
+
+node: test_pve
+        fabric_id test
+        interfaces name=ens20,passive=1,hello_interval=3,hello_multiplier=50
+        interfaces name=ens19,passive=1,csnp_interval=100
+        node_id pve
+        router_id 192.168.2.8
+
+node: test_pve1
+        fabric_id test
+        interfaces name=ens19
+        interfaces name=ens20
+        node_id pve1
+        router_id 192.168.2.20
+
+node: test_pve2
+        fabric_id test
+        interfaces name=ens19
+        interfaces name=ens20
+        node_id pve2
+        router_id 192.168.2.10
diff --git a/proxmox-ve-config/tests/fabric/cfg/openfabric_multi_fabric.cfg b/proxmox-ve-config/tests/fabric/cfg/openfabric_multi_fabric.cfg
new file mode 100644
index 000000000000..0c39505f44e5
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/openfabric_multi_fabric.cfg
@@ -0,0 +1,33 @@
+fabric: test1
+        hello_interval 4
+        loopback_prefix 192.168.2.0/24
+
+node: test1_pve
+        fabric_id test1
+        interfaces name=ens20,passive=1,hello_interval=3,hello_multiplier=50
+        interfaces name=ens19,passive=1,csnp_interval=100
+        node_id pve
+        router_id 192.168.2.8
+
+node: test1_pve1
+        fabric_id test1
+        interfaces name=ens19
+        node_id pve1
+        router_id 192.168.2.9
+
+fabric: test2
+        hello_interval 4
+        loopback_prefix 192.168.1.0/24
+
+node: test2_pve
+        fabric_id test2
+        interfaces name=ens20,passive=1,hello_interval=3,hello_multiplier=50
+        interfaces name=ens19,passive=1,csnp_interval=100
+        node_id pve
+        router_id 192.168.1.8
+
+node: test2_pve1
+        fabric_id test2
+        interfaces name=ens20
+        node_id pve1
+        router_id 192.168.1.9
diff --git a/proxmox-ve-config/tests/fabric/cfg/openfabric_verification_fail.cfg b/proxmox-ve-config/tests/fabric/cfg/openfabric_verification_fail.cfg
new file mode 100644
index 000000000000..00383bb24851
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/openfabric_verification_fail.cfg
@@ -0,0 +1,16 @@
+fabric: uwu
+        loopback_prefix 192.168.2.0/24
+
+node: uwu1_pve
+        fabric_id uwu1
+        interfaces name=ens20,passive=1,hello_interval=3,hello_multiplier=50
+        interfaces name=ens19,passive=1,csnp_interval=100
+        node_id pve
+        router_id 192.168.2.8
+
+node: uwu_pve1
+        fabric_id uwu
+        interfaces name=ens19
+        interfaces name=ens20
+        node_id pve1
+        router_id 192.168.2.9
diff --git a/proxmox-ve-config/tests/fabric/cfg/ospf_default.cfg b/proxmox-ve-config/tests/fabric/cfg/ospf_default.cfg
new file mode 100644
index 000000000000..d726c50a1642
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/ospf_default.cfg
@@ -0,0 +1,16 @@
+fabric: test
+        area 0
+        loopback_prefix 10.10.10.10/24
+
+node: test_pve
+        fabric_id test
+        interfaces name=ens18,passive=false,ip=4.4.4.4/24
+        interfaces name=ens19,passive=false,unnumbered=true
+        node_id pve
+        router_id 10.10.10.1
+
+node: test_pve1
+        fabric_id test
+        interfaces name=ens19,passive=false,unnumbered=true
+        node_id pve1
+        router_id 10.10.10.2
diff --git a/proxmox-ve-config/tests/fabric/cfg/ospf_interface_properties_validation_fail.cfg b/proxmox-ve-config/tests/fabric/cfg/ospf_interface_properties_validation_fail.cfg
new file mode 100644
index 000000000000..afa3f36b38e7
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/ospf_interface_properties_validation_fail.cfg
@@ -0,0 +1,10 @@
+fabric: test
+        area 0
+        loopback_prefix 10.10.10.0/24
+
+node: test_pve
+        fabric_id test
+        interfaces name=dummy0,passive=true
+        interfaces name=ens18,passive=false
+        node_id pve
+        router_id 10.10.10.1
diff --git a/proxmox-ve-config/tests/fabric/cfg/ospf_loopback_prefix_fail.cfg b/proxmox-ve-config/tests/fabric/cfg/ospf_loopback_prefix_fail.cfg
new file mode 100644
index 000000000000..7d170b6f7c4b
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/ospf_loopback_prefix_fail.cfg
@@ -0,0 +1,23 @@
+fabric: test
+        loopback_prefix 192.168.2.0/16
+
+node: test_pve
+        fabric_id test
+        interfaces name=ens20,passive=1,unnumbered=true
+        interfaces name=ens19,passive=1,unnumbered=true
+        node_id pve
+        router_id 192.168.2.8
+
+node: test_pve1
+        fabric_id test
+        interfaces name=ens19,unnumbered=true
+        interfaces name=ens20,unnumbered=true
+        node_id pve1
+        router_id 192.168.3.20
+
+node: test_pve2
+        fabric_id test
+        interfaces name=ens19,ip=3.3.3.2/31
+        interfaces name=ens20,ip=3.3.3.4/31
+        node_id pve2
+        router_id 192.169.2.10
diff --git a/proxmox-ve-config/tests/fabric/cfg/ospf_multi_fabric.cfg b/proxmox-ve-config/tests/fabric/cfg/ospf_multi_fabric.cfg
new file mode 100644
index 000000000000..0fb62bb2c9d8
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/ospf_multi_fabric.cfg
@@ -0,0 +1,33 @@
+fabric: test
+        area 0
+        loopback_prefix 192.168.2.0/24
+
+node: test_pve
+        fabric_id test
+        interfaces name=ens20,passive=1,unnumbered=true
+        interfaces name=ens19,passive=1,unnumbered=false,ip=3.3.3.4/31
+        node_id pve
+        router_id 192.168.2.8
+
+node: test_pve1
+        fabric_id test
+        interfaces name=ens19,unnumbered=true
+        node_id pve1
+        router_id 192.168.2.9
+
+fabric: ceph
+        area 1
+        loopback_prefix 192.168.1.0/24
+
+node: ceph_pve
+        fabric_id ceph
+        interfaces name=ens20,passive=1,unnumbered=true
+        interfaces name=ens19,passive=1,unnumbered=true
+        node_id pve
+        router_id 192.168.1.8
+
+node: ceph_pve1
+        fabric_id ceph
+        interfaces name=ens20,unnumbered=true
+        node_id pve1
+        router_id 192.168.1.9
diff --git a/proxmox-ve-config/tests/fabric/cfg/ospf_verification_fail.cfg b/proxmox-ve-config/tests/fabric/cfg/ospf_verification_fail.cfg
new file mode 100644
index 000000000000..f09636231320
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/ospf_verification_fail.cfg
@@ -0,0 +1,17 @@
+fabric: test
+        area 0
+        loopback_prefix 10.10.10.0/24
+
+node: test_pve
+        fabric_id test
+        interfaces name=dummy0,passive=true,unnumbered=true
+        interfaces name=ens18,passive=false,unnumbered=true
+        node_id pve
+        router_id 10.10.10.1
+
+node: test1_pve1
+        fabric_id test1
+        interfaces name=dummy0,passive=true,unnumbered=true
+        interfaces name=ens19,passive=false,unnumbered=true
+        node_id pve1
+        router_id 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 000000000000..f307c1b96b25
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/helper.rs
@@ -0,0 +1,43 @@
+#[allow(unused_macros)]
+macro_rules! get_section_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}.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_section_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 000000000000..2865b73266ea
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/main.rs
@@ -0,0 +1,132 @@
+#![cfg(feature = "frr")]
+use proxmox_frr::serializer::dump;
+use proxmox_ve_config::sdn::fabric::openfabric::OpenFabricSectionConfig;
+use proxmox_ve_config::sdn::fabric::ospf::OspfSectionConfig;
+use proxmox_ve_config::sdn::fabric::{FabricConfig, FrrConfigBuilder, Valid};
+
+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}.cfg" files.
+ * WIth the helper::reference_name!("<hostname>") macro you can get the snapshot file of this
+ * function for this specific hostname.
+ */
+
+#[test]
+fn openfabric_default() {
+    let openfabric =
+        <Valid<OpenFabricSectionConfig>>::parse_section_config("", &helper::get_section_config!())
+            .unwrap();
+
+    let config = FabricConfig::with_openfabric(openfabric);
+    let mut frr_config = FrrConfigBuilder::default()
+        .add_fabrics(config.clone())
+        .build("pve".parse().expect("valid id"))
+        .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("pve1".parse().expect("valid id"))
+        .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 ospf = <Valid<OspfSectionConfig>>::parse_section_config("", &helper::get_section_config!())
+        .unwrap();
+
+    let config = FabricConfig::with_ospf(ospf);
+    let mut frr_config = FrrConfigBuilder::default()
+        .add_fabrics(config.clone())
+        .build("pve".parse().expect("valid id"))
+        .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("pve1".parse().expect("valid id"))
+        .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 =
+        <Valid<OpenFabricSectionConfig>>::parse_section_config("", &helper::get_section_config!());
+    assert!(result.is_err());
+}
+
+#[test]
+fn ospf_verification_fail() {
+    let result =
+        <Valid<OspfSectionConfig>>::parse_section_config("", &helper::get_section_config!());
+    assert!(result.is_err());
+}
+
+#[test]
+fn openfabric_loopback_prefix_fail() {
+    let result =
+        <Valid<OpenFabricSectionConfig>>::parse_section_config("", &helper::get_section_config!());
+    assert!(result.is_err());
+}
+
+#[test]
+fn ospf_loopback_prefix_fail() {
+    let result =
+        <Valid<OspfSectionConfig>>::parse_section_config("", &helper::get_section_config!());
+    assert!(result.is_err());
+}
+
+#[test]
+fn openfabric_multi_fabric() {
+    let openfabric =
+        <Valid<OpenFabricSectionConfig>>::parse_section_config("", &helper::get_section_config!())
+            .unwrap();
+
+    let config = FabricConfig::with_openfabric(openfabric);
+    let frr_config = FrrConfigBuilder::default()
+        .add_fabrics(config.clone())
+        .build("pve1".parse().expect("valid id"))
+        .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 ospf = <Valid<OspfSectionConfig>>::parse_section_config("", &helper::get_section_config!())
+        .unwrap();
+
+    let config = FabricConfig::with_ospf(ospf);
+    let frr_config = FrrConfigBuilder::default()
+        .add_fabrics(config.clone())
+        .build("pve1".parse().expect("valid id"))
+        .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_interface_properties_validation_fail() {
+    let ospf = <Valid<OspfSectionConfig>>::parse_section_config("", &helper::get_section_config!());
+    assert!(ospf.is_err());
+}
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 000000000000..dfce9ac99ffe
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve.snap
@@ -0,0 +1,36 @@
+---
+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 passive
+ openfabric hello-interval 4
+ openfabric csnp-interval 100
+exit
+!
+interface ens20
+ ip router openfabric uwu
+ openfabric passive
+ openfabric hello-interval 3
+ openfabric hello-multiplier 50
+exit
+!
+access-list openfabric_uwu_ips permit 192.168.2.0/24
+!
+route-map openfabric permit 100
+ match ip address openfabric_uwu_ips
+ set src 192.168.2.8
+exit
+!
+ip protocol openfabric route-map 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 000000000000..5b30380ad47a
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_default_pve1.snap
@@ -0,0 +1,32 @@
+---
+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 openfabric_uwu_ips permit 192.168.2.0/24
+!
+route-map openfabric permit 100
+ match ip address openfabric_uwu_ips
+ set src 192.168.2.9
+exit
+!
+ip protocol openfabric route-map openfabric
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 000000000000..35d48e869164
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__openfabric_multi_fabric_pve1.snap
@@ -0,0 +1,48 @@
+---
+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 ens20
+ ip router openfabric test2
+ openfabric hello-interval 4
+exit
+!
+access-list openfabric_test1_ips permit 192.168.2.0/24
+!
+access-list openfabric_test2_ips permit 192.168.1.0/24
+!
+route-map openfabric permit 100
+ match ip address openfabric_test1_ips
+ set src 192.168.2.9
+exit
+!
+route-map openfabric permit 110
+ match ip address openfabric_test2_ips
+ set src 192.168.1.9
+exit
+!
+ip protocol openfabric route-map 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 000000000000..83b4006e6124
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve.snap
@@ -0,0 +1,31 @@
+---
+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 ospf_test_ips permit 10.10.10.10/24
+!
+route-map ospf permit 100
+ match ip address ospf_test_ips
+ set src 10.10.10.1
+exit
+!
+ip protocol ospf route-map 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 000000000000..c574bd9d99bd
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_default_pve1.snap
@@ -0,0 +1,27 @@
+---
+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 ospf_test_ips permit 10.10.10.10/24
+!
+route-map ospf permit 100
+ match ip address ospf_test_ips
+ set src 10.10.10.2
+exit
+!
+ip protocol ospf route-map 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 000000000000..22afba916120
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__ospf_multi_fabric_pve1.snap
@@ -0,0 +1,48 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+snapshot_kind: text
+---
+router ospf
+ ospf router-id 192.168.2.9
+exit
+!
+router ospf
+ ospf router-id 192.168.2.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 ens20
+ ip ospf area 1
+ ip ospf network point-to-point
+exit
+!
+access-list ospf_ceph_ips permit 192.168.1.0/24
+!
+access-list ospf_test_ips permit 192.168.2.0/24
+!
+route-map ospf permit 100
+ match ip address ospf_test_ips
+ set src 192.168.2.9
+exit
+!
+route-map ospf permit 110
+ match ip address ospf_ceph_ips
+ set src 192.168.1.9
+exit
+!
+ip protocol ospf route-map 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 v2 1/7] perl-rs: sdn: initial fabric infrastructure
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (15 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 15/15] ve-config: add integrations tests Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-perl-rs v2 2/7] perl-rs: sdn: add CRUD helpers for OpenFabric fabric management Gabriel Goller
                   ` (40 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

Add SDN fabric support with OpenFabric and OSPF configuration parsing.
Implements PerlSectionConfig wrapper and Perl module exports for fabric
configuration management.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 pve-rs/Cargo.toml         |  6 ++++-
 pve-rs/Makefile           |  1 +
 pve-rs/debian/control     |  5 ++++
 pve-rs/src/lib.rs         |  1 +
 pve-rs/src/sdn/fabrics.rs | 50 +++++++++++++++++++++++++++++++++++++++
 pve-rs/src/sdn/mod.rs     |  1 +
 6 files changed, 63 insertions(+), 1 deletion(-)
 create mode 100644 pve-rs/src/sdn/fabrics.rs
 create mode 100644 pve-rs/src/sdn/mod.rs

diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml
index f2e495d33743..bdfff6adf685 100644
--- a/pve-rs/Cargo.toml
+++ b/pve-rs/Cargo.toml
@@ -33,15 +33,19 @@ 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-network-types = { version = "0.1" }
 proxmox-notify = { version = "0.5", features = ["pve-context"] }
 proxmox-openid = "0.10"
 proxmox-resource-scheduling = "0.3.0"
+proxmox-schema = "4.0.0"
+proxmox-section-config = "2.1.1"
 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 d01da692d8c9..86af16eb5e04 100644
--- a/pve-rs/Makefile
+++ b/pve-rs/Makefile
@@ -31,6 +31,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 8dadddf3d74f..5e3b8f662ad0 100644
--- a/pve-rs/debian/control
+++ b/pve-rs/debian/control
@@ -18,15 +18,19 @@ 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,
                librust-proxmox-http-error-0.1+default-dev,
                librust-proxmox-log-0.2+default-dev,
+               librust-proxmox-network-types-0.1+default-dev <!nocheck>,
                librust-proxmox-notify-0.5+default-dev,
                librust-proxmox-notify-0.5+pve-context-dev,
                librust-proxmox-openid-0.10+default-dev,
                librust-proxmox-resource-scheduling-0.3+default-dev,
+               librust-proxmox-schema-4+default-dev <!nocheck>,
+               librust-proxmox-section-config-2+default-dev (>= 2.1.2-~~) <!nocheck>,
                librust-proxmox-shared-cache-0.1+default-dev,
                librust-proxmox-subscription-0.5+default-dev,
                librust-proxmox-sys-0.6+default-dev,
@@ -34,6 +38,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.1-~~),
                librust-serde-1+default-dev,
                librust-serde-bytes-0.11+default-dev,
                librust-serde-json-1+default-dev,
diff --git a/pve-rs/src/lib.rs b/pve-rs/src/lib.rs
index 3de37d17fab6..12ee87a91cc6 100644
--- a/pve-rs/src/lib.rs
+++ b/pve-rs/src/lib.rs
@@ -15,6 +15,7 @@ pub mod apt;
 pub mod firewall;
 pub mod openid;
 pub mod resource_scheduling;
+pub mod sdn;
 pub mod tfa;
 
 #[perlmod::package(name = "Proxmox::Lib::PVE", lib = "pve_rs")]
diff --git a/pve-rs/src/sdn/fabrics.rs b/pve-rs/src/sdn/fabrics.rs
new file mode 100644
index 000000000000..a761cea36ec0
--- /dev/null
+++ b/pve-rs/src/sdn/fabrics.rs
@@ -0,0 +1,50 @@
+#[perlmod::package(name = "PVE::RS::SDN::Fabrics", lib = "pve_rs")]
+pub mod export {
+    use std::sync::Mutex;
+
+    use anyhow::Error;
+    use proxmox_section_config::{
+        typed::ApiSectionDataEntry, typed::SectionConfigData as TypedSectionConfigData,
+    };
+    use proxmox_ve_config::sdn::fabric::{
+        openfabric::OpenFabricSectionConfig, ospf::OspfSectionConfig,
+    };
+    use serde::{Deserialize, Serialize};
+
+    pub struct PerlSectionConfig<T> {
+        pub section_config: Mutex<TypedSectionConfigData<T>>,
+    }
+
+    impl<T> PerlSectionConfig<T>
+    where
+        T: Send + Sync + Clone,
+    {
+        pub fn into_inner(self) -> Result<TypedSectionConfigData<T>, anyhow::Error> {
+            let value = self.section_config.into_inner().unwrap();
+            Ok(value)
+        }
+    }
+
+    #[derive(Serialize, Deserialize)]
+    struct AllConfigs {
+        openfabric: Vec<OpenFabricSectionConfig>,
+        ospf: Vec<OspfSectionConfig>,
+    }
+
+    /// Get all the config. This takes the raw openfabric and ospf config, parses, and returns
+    /// both.
+    #[export]
+    fn config(raw_openfabric: &[u8], raw_ospf: &[u8]) -> Result<AllConfigs, Error> {
+        let raw_openfabric = std::str::from_utf8(raw_openfabric)?;
+        let raw_ospf = std::str::from_utf8(raw_ospf)?;
+
+        let openfabric =
+            OpenFabricSectionConfig::parse_section_config("openfabric.cfg", raw_openfabric)?;
+        let ospf = OspfSectionConfig::parse_section_config("ospf.cfg", raw_ospf)?;
+
+        Ok(AllConfigs {
+            openfabric: openfabric.into_iter().map(|e| e.1).collect(),
+            ospf: ospf.into_iter().map(|e| e.1).collect(),
+        })
+    }
+}
diff --git a/pve-rs/src/sdn/mod.rs b/pve-rs/src/sdn/mod.rs
new file mode 100644
index 000000000000..3e3b1376f8d6
--- /dev/null
+++ b/pve-rs/src/sdn/mod.rs
@@ -0,0 +1 @@
+pub 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 v2 2/7] perl-rs: sdn: add CRUD helpers for OpenFabric fabric management
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (16 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-perl-rs v2 1/7] perl-rs: sdn: initial fabric infrastructure Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-perl-rs v2 3/7] perl-rs: sdn: OpenFabric perlmod methods Gabriel Goller
                   ` (39 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

Add functionality for managing OpenFabric fabrics:
- Implement Rust-backed Perl module PVE::RS::SDN::Fabrics::OpenFabric
- Add CRUD methods for fabric, node, and interface configuration
- Support fabric-specific parameters (hello-intervals, router-id, etc.)

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 pve-rs/Cargo.toml            |   1 +
 pve-rs/Makefile              |   1 +
 pve-rs/debian/control        |   1 +
 pve-rs/src/sdn/mod.rs        |   1 +
 pve-rs/src/sdn/openfabric.rs | 222 +++++++++++++++++++++++++++++++++++
 5 files changed, 226 insertions(+)
 create mode 100644 pve-rs/src/sdn/openfabric.rs

diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml
index bdfff6adf685..5abb960d2668 100644
--- a/pve-rs/Cargo.toml
+++ b/pve-rs/Cargo.toml
@@ -42,6 +42,7 @@ proxmox-notify = { version = "0.5", features = ["pve-context"] }
 proxmox-openid = "0.10"
 proxmox-resource-scheduling = "0.3.0"
 proxmox-schema = "4.0.0"
+proxmox-sdn-types = { version = "0.1" }
 proxmox-section-config = "2.1.1"
 proxmox-shared-cache = "0.1.0"
 proxmox-subscription = "0.5"
diff --git a/pve-rs/Makefile b/pve-rs/Makefile
index 86af16eb5e04..6bd9c8a2acec 100644
--- a/pve-rs/Makefile
+++ b/pve-rs/Makefile
@@ -32,6 +32,7 @@ PERLMOD_PACKAGES := \
 	  PVE::RS::OpenId \
 	  PVE::RS::ResourceScheduling::Static \
 	  PVE::RS::SDN::Fabrics \
+	  PVE::RS::SDN::Fabrics::OpenFabric \
 	  PVE::RS::TFA
 
 PERLMOD_PACKAGE_FILES := $(addsuffix .pm,$(subst ::,/,$(PERLMOD_PACKAGES)))
diff --git a/pve-rs/debian/control b/pve-rs/debian/control
index 5e3b8f662ad0..61631c4932ee 100644
--- a/pve-rs/debian/control
+++ b/pve-rs/debian/control
@@ -30,6 +30,7 @@ Build-Depends: cargo:native <!nocheck>,
                librust-proxmox-openid-0.10+default-dev,
                librust-proxmox-resource-scheduling-0.3+default-dev,
                librust-proxmox-schema-4+default-dev <!nocheck>,
+               librust-proxmox-sdn-types-0.1+default-dev <!nocheck>,
                librust-proxmox-section-config-2+default-dev (>= 2.1.2-~~) <!nocheck>,
                librust-proxmox-shared-cache-0.1+default-dev,
                librust-proxmox-subscription-0.5+default-dev,
diff --git a/pve-rs/src/sdn/mod.rs b/pve-rs/src/sdn/mod.rs
index 3e3b1376f8d6..36afb099ece0 100644
--- a/pve-rs/src/sdn/mod.rs
+++ b/pve-rs/src/sdn/mod.rs
@@ -1 +1,2 @@
 pub mod fabrics;
+pub mod openfabric;
diff --git a/pve-rs/src/sdn/openfabric.rs b/pve-rs/src/sdn/openfabric.rs
new file mode 100644
index 000000000000..569775857755
--- /dev/null
+++ b/pve-rs/src/sdn/openfabric.rs
@@ -0,0 +1,222 @@
+#[perlmod::package(name = "PVE::RS::SDN::Fabrics::OpenFabric", lib = "pve_rs")]
+mod export {
+    use std::{net::IpAddr, str};
+
+    use anyhow::Context;
+
+    use proxmox_network_types::{debian::Hostname, ip_address::Cidr};
+    use proxmox_schema::property_string::PropertyString;
+    use proxmox_sdn_types::openfabric::{CsnpInterval, HelloInterval, HelloMultiplier};
+    use proxmox_section_config::typed::{ApiSectionDataEntry, SectionConfigData};
+    use proxmox_ve_config::sdn::fabric::{
+        FabricId, NodeId, SectionType, Validate,
+        openfabric::{FabricSection, InterfaceProperties, NodeSection, OpenFabricSectionConfig},
+    };
+    use serde::{Deserialize, Serialize};
+
+    use crate::sdn::fabrics::export::PerlSectionConfig;
+
+    perlmod::declare_magic!(Box<PerlSectionConfig<OpenFabricSectionConfig>> : &PerlSectionConfig<OpenFabricSectionConfig> as "PVE::RS::SDN::Fabrics::OpenFabric");
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct AddFabric {
+        fabric_id: FabricId,
+        hello_interval: Option<HelloInterval>,
+        loopback_prefix: Cidr,
+    }
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct DeleteFabric {
+        fabric_id: FabricId,
+    }
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct DeleteNode {
+        fabric_id: FabricId,
+        node_id: Hostname,
+    }
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct DeleteInterface {
+        fabric_id: FabricId,
+        node_id: Hostname,
+        /// interface name
+        name: String,
+    }
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct EditFabric {
+        fabric_id: FabricId,
+        hello_interval: Option<HelloInterval>,
+    }
+
+    #[derive(Debug, Deserialize)]
+    pub struct AddNode {
+        fabric_id: FabricId,
+        node_id: Hostname,
+        router_id: IpAddr,
+        interfaces: Option<Vec<PropertyString<InterfaceProperties>>>,
+    }
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct EditNode {
+        node_id: Hostname,
+        fabric_id: FabricId,
+        router_id: IpAddr,
+        interfaces: Option<Vec<PropertyString<InterfaceProperties>>>,
+    }
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct EditInterface {
+        node_id: Hostname,
+        fabric_id: FabricId,
+        name: String,
+        passive: bool,
+        hello_interval: Option<HelloInterval>,
+        hello_multiplier: Option<HelloMultiplier>,
+        csnp_interval: Option<CsnpInterval>,
+    }
+
+    fn interface_exists(
+        config: &SectionConfigData<OpenFabricSectionConfig>,
+        interface_name: &str,
+        node_name: &str,
+    ) -> bool {
+        config.sections.iter().any(|(k, v)| {
+            if let OpenFabricSectionConfig::Node(n) = v {
+                k.parse::<NodeId>().ok().is_some_and(|id| {
+                    id.node_id.as_ref() == node_name
+                        && n.interfaces.iter().any(|i| i.name == interface_name)
+                })
+            } else {
+                false
+            }
+        })
+    }
+
+    impl PerlSectionConfig<OpenFabricSectionConfig> {
+        pub fn add_fabric(&self, params: AddFabric) -> Result<(), anyhow::Error> {
+            let mut config = self.section_config.lock().unwrap();
+            if config.sections.contains_key(params.fabric_id.as_ref()) {
+                anyhow::bail!("fabric already exists");
+            }
+            let new_fabric = OpenFabricSectionConfig::Fabric(FabricSection {
+                fabric_id: params.fabric_id.clone(),
+                hello_interval: params.hello_interval,
+                ty: SectionType::Fabric,
+                loopback_prefix: params.loopback_prefix,
+            });
+            config
+                .sections
+                .insert(params.fabric_id.to_string(), new_fabric);
+
+            config.order.push(params.fabric_id.to_string());
+            OpenFabricSectionConfig::validate_as_ref(&config)?;
+            Ok(())
+        }
+
+        pub fn add_node(&self, params: AddNode) -> Result<(), anyhow::Error> {
+            let id = NodeId::new(params.fabric_id.clone(), params.node_id.clone());
+            let id_string = id.to_string();
+
+            let mut config = self.section_config.lock().unwrap();
+
+            if config.sections.contains_key(&id_string) {
+                anyhow::bail!("node already exists");
+            }
+
+            if let Some(interfaces) = &params.interfaces {
+                if interfaces
+                    .iter()
+                    .any(|i| interface_exists(&config, &i.name, id_string.as_ref()))
+                {
+                    anyhow::bail!("One interface cannot be a part of two fabrics");
+                }
+            }
+            let new_fabric = OpenFabricSectionConfig::Node(NodeSection {
+                id,
+                node_id: params.node_id,
+                fabric_id: params.fabric_id,
+                router_id: params.router_id,
+                interfaces: params.interfaces.unwrap_or_default(),
+                ty: SectionType::Node,
+            });
+            config.sections.insert(id_string.clone(), new_fabric);
+            config.order.push(id_string);
+            OpenFabricSectionConfig::validate_as_ref(&config)?;
+
+            Ok(())
+        }
+
+        pub fn edit_fabric(&self, params: EditFabric) -> Result<(), anyhow::Error> {
+            let mut config = self.section_config.lock().unwrap();
+
+            let fabricid = params.fabric_id;
+
+            if let OpenFabricSectionConfig::Fabric(fs) = config
+                .sections
+                .get_mut(fabricid.as_ref())
+                .context("fabric doesn't exist")?
+            {
+                fs.hello_interval = params.hello_interval;
+            }
+            OpenFabricSectionConfig::validate_as_ref(&config)?;
+            Ok(())
+        }
+
+        pub fn edit_node(&self, params: EditNode) -> Result<(), anyhow::Error> {
+            let router_id = params.router_id;
+
+            let nodeid = NodeId::new(params.fabric_id, params.node_id).to_string();
+
+            let mut config = self.section_config.lock().unwrap();
+            if let Some(node) = config.sections.get_mut(&nodeid) {
+                if let OpenFabricSectionConfig::Node(n) = node {
+                    n.router_id = router_id;
+                    n.interfaces = params.interfaces.unwrap_or_default();
+                }
+            } else {
+                anyhow::bail!("node not found");
+            }
+
+            OpenFabricSectionConfig::validate_as_ref(&config)?;
+            Ok(())
+        }
+
+        pub fn delete_fabric(&self, params: DeleteFabric) -> Result<(), anyhow::Error> {
+            let mut config = self.section_config.lock().unwrap();
+
+            let fabricid = params.fabric_id;
+
+            config
+                .sections
+                .remove(fabricid.as_ref())
+                .ok_or(anyhow::anyhow!("fabric not found"))?;
+            // remove all the nodes
+            config.sections.retain(|k, _v| {
+                if let Ok(nodeid) = k.parse::<NodeId>() {
+                    return nodeid.fabric_id != fabricid;
+                }
+                true
+            });
+            OpenFabricSectionConfig::validate_as_ref(&config)?;
+            Ok(())
+        }
+
+        pub fn delete_node(&self, params: DeleteNode) -> Result<(), anyhow::Error> {
+            let mut config = self.section_config.lock().unwrap();
+            let nodeid = NodeId::new(params.fabric_id, params.node_id).to_string();
+            config
+                .sections
+                .remove(&nodeid)
+                .ok_or(anyhow::anyhow!("node not found"))?;
+            OpenFabricSectionConfig::validate_as_ref(&config)?;
+            Ok(())
+        }
+
+        pub fn write(&self) -> Result<String, anyhow::Error> {
+            let guard = self.section_config.lock().unwrap().clone();
+            OpenFabricSectionConfig::write_section_config("sdn/fabrics/openfabric.cfg", &guard)
+        }
+    }
+}
-- 
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 v2 3/7] perl-rs: sdn: OpenFabric perlmod methods
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (17 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-perl-rs v2 2/7] perl-rs: sdn: add CRUD helpers for OpenFabric fabric management Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-perl-rs v2 4/7] perl-rs: sdn: implement Openfabric interface file generation Gabriel Goller
                   ` (38 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

Add perlmod methods that call the previously introduced CRUD helpers.
Also add a method that returns the FRR daemons to be enabled by
pve-network and a method to validate and generate the FRR config.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 pve-rs/src/sdn/openfabric.rs | 174 ++++++++++++++++++++++++++++++++++-
 1 file changed, 171 insertions(+), 3 deletions(-)

diff --git a/pve-rs/src/sdn/openfabric.rs b/pve-rs/src/sdn/openfabric.rs
index 569775857755..e7d46ee765bc 100644
--- a/pve-rs/src/sdn/openfabric.rs
+++ b/pve-rs/src/sdn/openfabric.rs
@@ -1,15 +1,17 @@
 #[perlmod::package(name = "PVE::RS::SDN::Fabrics::OpenFabric", lib = "pve_rs")]
 mod export {
-    use std::{net::IpAddr, str};
+    use std::{collections::HashMap, net::IpAddr, str, sync::Mutex};
 
-    use anyhow::Context;
+    use anyhow::{Context, Error};
 
+    use perlmod::Value;
+    use proxmox_frr::serializer::to_raw_config;
     use proxmox_network_types::{debian::Hostname, ip_address::Cidr};
     use proxmox_schema::property_string::PropertyString;
     use proxmox_sdn_types::openfabric::{CsnpInterval, HelloInterval, HelloMultiplier};
     use proxmox_section_config::typed::{ApiSectionDataEntry, SectionConfigData};
     use proxmox_ve_config::sdn::fabric::{
-        FabricId, NodeId, SectionType, Validate,
+        FabricConfig, FabricId, FrrConfigBuilder, NodeId, SectionType, Valid, Validate,
         openfabric::{FabricSection, InterfaceProperties, NodeSection, OpenFabricSectionConfig},
     };
     use serde::{Deserialize, Serialize};
@@ -219,4 +221,170 @@ mod export {
             OpenFabricSectionConfig::write_section_config("sdn/fabrics/openfabric.cfg", &guard)
         }
     }
+
+    #[export(raw_return)]
+    fn running_config(
+        #[raw] class: Value,
+        raw_config: HashMap<String, OpenFabricSectionConfig>,
+    ) -> Result<perlmod::Value, anyhow::Error> {
+        // we cannot just construct it from the HashMap via From, since then the order is empty
+        let section_config = raw_config.into_iter().collect();
+
+        let return_value = PerlSectionConfig {
+            section_config: Mutex::new(section_config),
+        };
+
+        Ok(perlmod::instantiate_magic!(&class, MAGIC => Box::new(
+                return_value
+        )))
+    }
+
+    #[export(raw_return)]
+    fn config(#[raw] class: Value, raw_config: &[u8]) -> Result<perlmod::Value, anyhow::Error> {
+        let raw_config = std::str::from_utf8(raw_config)?;
+
+        let config = OpenFabricSectionConfig::parse_section_config("openfabric.cfg", raw_config)?;
+        let return_value = PerlSectionConfig {
+            section_config: Mutex::new(config),
+        };
+
+        Ok(perlmod::instantiate_magic!(&class, MAGIC => Box::new(
+                return_value
+        )))
+    }
+
+    /// Writes the config to a string and returns the configuration and the protocol.
+    #[export]
+    fn write(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+    ) -> Result<(String, String), Error> {
+        let full_new_config = this.write()?;
+
+        // We return the protocol here as well, so that in perl we can write to
+        // the correct config file
+        Ok((full_new_config, "openfabric".to_string()))
+    }
+
+    #[export]
+    fn add_fabric(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+        params: AddFabric,
+    ) -> Result<(), Error> {
+        this.add_fabric(params)?;
+
+        Ok(())
+    }
+
+    #[export]
+    fn add_node(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+        params: AddNode,
+    ) -> Result<(), Error> {
+        this.add_node(params)
+    }
+
+    #[export]
+    fn edit_fabric(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+        params: EditFabric,
+    ) -> Result<(), Error> {
+        this.edit_fabric(params)
+    }
+
+    #[export]
+    fn edit_node(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+        params: EditNode,
+    ) -> Result<(), Error> {
+        this.edit_node(params)
+    }
+
+    #[export]
+    fn delete_fabric(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+        params: DeleteFabric,
+    ) -> Result<(), Error> {
+        this.delete_fabric(params)?;
+
+        Ok(())
+    }
+
+    #[export]
+    fn delete_node(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+        params: DeleteNode,
+    ) -> Result<(), Error> {
+        this.delete_node(params)?;
+
+        Ok(())
+    }
+
+    #[export]
+    fn get_inner(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+    ) -> HashMap<String, OpenFabricSectionConfig> {
+        let guard = this.section_config.lock().unwrap();
+        guard.clone().into_iter().collect()
+    }
+
+    #[export]
+    fn get_fabric(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+        fabric: FabricId,
+    ) -> Result<OpenFabricSectionConfig, Error> {
+        let guard = this.section_config.lock().unwrap();
+        guard
+            .get(fabric.as_ref())
+            .cloned()
+            .ok_or(anyhow::anyhow!("fabric not found"))
+    }
+
+    #[export]
+    fn get_node(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+        fabric: FabricId,
+        node: Hostname,
+    ) -> Result<OpenFabricSectionConfig, Error> {
+        let guard = this.section_config.lock().unwrap();
+        let nodeid = NodeId::new(fabric, node).to_string();
+        guard
+            .get(&nodeid)
+            .cloned()
+            .ok_or(anyhow::anyhow!("node not found"))
+    }
+
+    #[export]
+    pub fn enabled_daemons(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+        hostname: Hostname,
+    ) -> Vec<String> {
+        let config = this.section_config.lock().unwrap();
+
+        for (_, section) in config.iter() {
+            if let OpenFabricSectionConfig::Node(node_config) = section {
+                if node_config.node_id == hostname {
+                    return vec!["fabricd".to_string()];
+                }
+            }
+        }
+
+        Vec::new()
+    }
+
+    #[export]
+    pub fn get_frr_raw_config(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+        node: Hostname,
+    ) -> Result<Vec<String>, Error> {
+        let config = this.section_config.lock().unwrap();
+        let openfabric_config: Valid<OpenFabricSectionConfig> =
+            OpenFabricSectionConfig::validate(config.clone())?;
+
+        let config = FabricConfig::with_openfabric(openfabric_config);
+        let frr_config = FrrConfigBuilder::default()
+            .add_fabrics(config)
+            .build(node)?;
+
+        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 v2 4/7] perl-rs: sdn: implement Openfabric interface file generation
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (18 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-perl-rs v2 3/7] perl-rs: sdn: OpenFabric perlmod methods Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-perl-rs v2 5/7] perl-rs: sdn: add CRUD helpers for OSPF fabric management Gabriel Goller
                   ` (37 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

Add function to generate /etc/network/interfaces configuration for OpenFabric nodes:
- Auto-create dummy interfaces with proper router-id
- Configure interface addresses and IP forwarding
- Support for both IPv4 and IPv6 addressing on both dummy and other
  interfaces

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 pve-rs/src/sdn/openfabric.rs | 74 +++++++++++++++++++++++++++++++++++-
 1 file changed, 73 insertions(+), 1 deletion(-)

diff --git a/pve-rs/src/sdn/openfabric.rs b/pve-rs/src/sdn/openfabric.rs
index e7d46ee765bc..5c14de04987f 100644
--- a/pve-rs/src/sdn/openfabric.rs
+++ b/pve-rs/src/sdn/openfabric.rs
@@ -1,6 +1,6 @@
 #[perlmod::package(name = "PVE::RS::SDN::Fabrics::OpenFabric", lib = "pve_rs")]
 mod export {
-    use std::{collections::HashMap, net::IpAddr, str, sync::Mutex};
+    use std::{collections::HashMap, fmt::Write, net::IpAddr, str, sync::Mutex};
 
     use anyhow::{Context, Error};
 
@@ -353,6 +353,78 @@ mod export {
             .ok_or(anyhow::anyhow!("node not found"))
     }
 
+    #[export]
+    fn get_interfaces_etc_network_config(
+        #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
+        node: Hostname,
+    ) -> Result<String, Error> {
+        let guard = this.section_config.lock().unwrap();
+        let mut interfaces = String::new();
+
+        guard.iter().try_for_each(|section| {
+            if let OpenFabricSectionConfig::Node(node_section) = section.1 {
+                if node_section.node_id == node {
+                    // create dummy interface for this fabric
+                    writeln!(interfaces)?;
+                    writeln!(interfaces, "auto dummy_{}", node_section.fabric_id)?;
+                    match node_section.router_id {
+                        IpAddr::V4(_) => writeln!(
+                            interfaces,
+                            "iface dummy_{} inet static",
+                            node_section.fabric_id
+                        )?,
+                        IpAddr::V6(_) => writeln!(
+                            interfaces,
+                            "iface dummy_{} inet6 static",
+                            node_section.fabric_id
+                        )?,
+                    }
+                    writeln!(interfaces, "\tlink-type dummy")?;
+                    writeln!(interfaces, "\tip-forward 1")?;
+                    // add dummy interface address as /32
+                    match node_section.router_id {
+                        IpAddr::V4(ipv4_addr) => {
+                            writeln!(interfaces, "\taddress {}/32", ipv4_addr)?
+                        }
+                        IpAddr::V6(ipv6_addr) => {
+                            writeln!(interfaces, "\taddress {}/128", ipv6_addr)?
+                        }
+                    }
+
+                    // add ip-addrs to all other interfaces and ensure they exist
+                    // also enable ip-forwarding on all interfaces as this is needed for unnumbered
+                    // peering
+                    node_section
+                        .clone()
+                        .interfaces
+                        .into_iter()
+                        .try_for_each(|i| {
+                            let interface_name = i.name.clone();
+                            writeln!(interfaces)?;
+                            writeln!(interfaces, "auto {interface_name}")?;
+                            if let Some(ip) = i.ip.map(|i| i.to_string()) {
+                                writeln!(interfaces, "iface {interface_name} inet static")?;
+                                writeln!(interfaces, "\taddress {}", ip)?;
+                                writeln!(interfaces, "\tip-forward 1")?;
+                            }
+                            if let Some(ipv6) = i.ipv6.map(|i| i.to_string()) {
+                                writeln!(interfaces, "iface {interface_name} inet6 static")?;
+                                writeln!(interfaces, "\taddress {ipv6}")?;
+                                writeln!(interfaces, "\tip6-forward 1")?;
+                            }
+                            if i.ip.is_none() && i.ipv6.is_none() {
+                                writeln!(interfaces, "iface {interface_name} inet manual")?;
+                                writeln!(interfaces, "\tip-forward 1")?;
+                            }
+                            Ok::<(), std::fmt::Error>(())
+                        })?;
+                }
+            }
+            Ok::<(), std::fmt::Error>(())
+        })?;
+        Ok(interfaces)
+    }
+
     #[export]
     pub fn enabled_daemons(
         #[try_from_ref] this: &PerlSectionConfig<OpenFabricSectionConfig>,
-- 
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 v2 5/7] perl-rs: sdn: add CRUD helpers for OSPF fabric management
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (19 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-perl-rs v2 4/7] perl-rs: sdn: implement Openfabric interface file generation Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-perl-rs v2 6/7] perl-rs: sdn: OSPF perlmod methods Gabriel Goller
                   ` (36 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

Add CRUD functions for managing OSPF fabrics.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 pve-rs/Makefile        |   1 +
 pve-rs/src/sdn/mod.rs  |   1 +
 pve-rs/src/sdn/ospf.rs | 217 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 219 insertions(+)
 create mode 100644 pve-rs/src/sdn/ospf.rs

diff --git a/pve-rs/Makefile b/pve-rs/Makefile
index 6bd9c8a2acec..5bd4d3c58b36 100644
--- a/pve-rs/Makefile
+++ b/pve-rs/Makefile
@@ -33,6 +33,7 @@ PERLMOD_PACKAGES := \
 	  PVE::RS::ResourceScheduling::Static \
 	  PVE::RS::SDN::Fabrics \
 	  PVE::RS::SDN::Fabrics::OpenFabric \
+	  PVE::RS::SDN::Fabrics::Ospf \
 	  PVE::RS::TFA
 
 PERLMOD_PACKAGE_FILES := $(addsuffix .pm,$(subst ::,/,$(PERLMOD_PACKAGES)))
diff --git a/pve-rs/src/sdn/mod.rs b/pve-rs/src/sdn/mod.rs
index 36afb099ece0..6700c989483f 100644
--- a/pve-rs/src/sdn/mod.rs
+++ b/pve-rs/src/sdn/mod.rs
@@ -1,2 +1,3 @@
 pub mod fabrics;
 pub mod openfabric;
+pub mod ospf;
diff --git a/pve-rs/src/sdn/ospf.rs b/pve-rs/src/sdn/ospf.rs
new file mode 100644
index 000000000000..4ab1e7e505f0
--- /dev/null
+++ b/pve-rs/src/sdn/ospf.rs
@@ -0,0 +1,217 @@
+#[perlmod::package(name = "PVE::RS::SDN::Fabrics::Ospf", lib = "pve_rs")]
+mod export {
+    use std::{net::Ipv4Addr, str};
+
+    use anyhow::Context;
+
+    use proxmox_network_types::{debian::Hostname, ip_address::Ipv4Cidr};
+    use proxmox_schema::property_string::PropertyString;
+    use proxmox_section_config::typed::{ApiSectionDataEntry, SectionConfigData};
+    use proxmox_ve_config::sdn::fabric::{
+        FabricId, NodeId, SectionType, Validate,
+        ospf::{Area, FabricSection, InterfaceProperties, NodeSection, OspfSectionConfig},
+    };
+    use serde::{Deserialize, Serialize};
+
+    use crate::sdn::fabrics::export::PerlSectionConfig;
+
+    perlmod::declare_magic!(Box<PerlSectionConfig<OspfSectionConfig>> : &PerlSectionConfig<OspfSectionConfig> as "PVE::RS::SDN::Fabrics::Ospf");
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct AddFabric {
+        fabric_id: FabricId,
+        area: Area,
+        loopback_prefix: Ipv4Cidr,
+    }
+
+    #[derive(Debug, Deserialize)]
+    pub struct AddNode {
+        node_id: Hostname,
+        fabric_id: FabricId,
+        router_id: Ipv4Addr,
+        interfaces: Option<Vec<PropertyString<InterfaceProperties>>>,
+    }
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct DeleteFabric {
+        fabric_id: FabricId,
+    }
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct DeleteNode {
+        fabric_id: FabricId,
+        node_id: Hostname,
+    }
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct DeleteInterface {
+        fabric_id: FabricId,
+        node_id: Hostname,
+        /// interface name
+        name: String,
+    }
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct EditFabric {
+        fabric_id: FabricId,
+        area: Area,
+    }
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct EditNode {
+        fabric_id: FabricId,
+        node_id: Hostname,
+
+        router_id: Ipv4Addr,
+        interfaces: Option<Vec<PropertyString<InterfaceProperties>>>,
+    }
+
+    #[derive(Debug, Serialize, Deserialize)]
+    pub struct EditInterface {
+        fabric_id: FabricId,
+        node_id: Hostname,
+        name: String,
+
+        passive: bool,
+    }
+
+    fn interface_exists(
+        config: &SectionConfigData<OspfSectionConfig>,
+        interface_name: &str,
+        node_name: &str,
+    ) -> bool {
+        config.sections.iter().any(|(k, v)| {
+            if let OspfSectionConfig::Node(n) = v {
+                k.parse::<NodeId>().ok().is_some_and(|id| {
+                    id.node_id.as_ref() == node_name
+                        && n.interfaces.iter().any(|i| i.name == interface_name)
+                })
+            } else {
+                false
+            }
+        })
+    }
+
+    impl PerlSectionConfig<OspfSectionConfig> {
+        pub fn add_fabric(&self, params: AddFabric) -> Result<(), anyhow::Error> {
+            let fabric_id = params.fabric_id.to_string();
+
+            let mut config = self.section_config.lock().unwrap();
+
+            if config.sections.contains_key(&fabric_id) {
+                anyhow::bail!("fabric already exists");
+            }
+
+            let new_fabric = OspfSectionConfig::Fabric(FabricSection {
+                fabric_id: params.fabric_id,
+                area: params.area,
+                ty: SectionType::Fabric,
+                loopback_prefix: params.loopback_prefix,
+            });
+
+            config.sections.insert(fabric_id.clone(), new_fabric);
+            config.order.push(fabric_id);
+
+            OspfSectionConfig::validate_as_ref(&config)?;
+            Ok(())
+        }
+
+        pub fn add_node(&self, params: AddNode) -> Result<(), anyhow::Error> {
+            let id = NodeId::new(params.fabric_id.clone(), params.node_id.clone());
+            let id_string = id.to_string();
+
+            let mut config = self.section_config.lock().unwrap();
+            if config.sections.contains_key(&id_string) {
+                anyhow::bail!("node already exists");
+            }
+
+            if let Some(interfaces) = &params.interfaces {
+                if interfaces
+                    .iter()
+                    .any(|i| interface_exists(&config, &i.name, id_string.as_ref()))
+                {
+                    anyhow::bail!("One interface cannot be a part of two fabrics");
+                }
+            }
+
+            let new_fabric = OspfSectionConfig::Node(NodeSection {
+                id,
+                fabric_id: params.fabric_id,
+                node_id: params.node_id,
+                router_id: params.router_id,
+                interfaces: params.interfaces.unwrap_or_default(),
+                ty: SectionType::Node,
+            });
+            config.sections.insert(id_string.clone(), new_fabric);
+            config.order.push(id_string);
+            OspfSectionConfig::validate_as_ref(&config)?;
+            Ok(())
+        }
+
+        pub fn edit_fabric(&self, params: EditFabric) -> Result<(), anyhow::Error> {
+            let mut config = self.section_config.lock().unwrap();
+
+            if let OspfSectionConfig::Fabric(fs) = config
+                .sections
+                .get_mut(params.fabric_id.as_ref())
+                .context("fabric doesn't exist")?
+            {
+                fs.area = params.area;
+            }
+            OspfSectionConfig::validate_as_ref(&config)?;
+            Ok(())
+        }
+
+        pub fn edit_node(&self, params: EditNode) -> Result<(), anyhow::Error> {
+            let nodeid = NodeId::new(params.fabric_id, params.node_id).to_string();
+
+            let mut config = self.section_config.lock().unwrap();
+            if let Some(node) = config.sections.get_mut(&nodeid) {
+                if let OspfSectionConfig::Node(n) = node {
+                    n.router_id = params.router_id;
+                    n.interfaces = params.interfaces.unwrap_or_default();
+                }
+            } else {
+                anyhow::bail!("node not found");
+            }
+            OspfSectionConfig::validate_as_ref(&config)?;
+            Ok(())
+        }
+
+        pub fn delete_fabric(&self, params: DeleteFabric) -> Result<(), anyhow::Error> {
+            let mut config = self.section_config.lock().unwrap();
+
+            let fabric_id = params.fabric_id;
+            config
+                .sections
+                .remove(fabric_id.as_ref())
+                .ok_or(anyhow::anyhow!("no fabric found"))?;
+
+            // remove all the nodes
+            config.sections.retain(|k, _v| {
+                if let Ok(nodeid) = k.parse::<NodeId>() {
+                    return nodeid.fabric_id != fabric_id;
+                }
+                true
+            });
+            OspfSectionConfig::validate_as_ref(&config)?;
+            Ok(())
+        }
+
+        pub fn delete_node(&self, params: DeleteNode) -> Result<(), anyhow::Error> {
+            let mut config = self.section_config.lock().unwrap();
+            let nodeid = NodeId::new(params.fabric_id, params.node_id).to_string();
+            config
+                .sections
+                .remove(&nodeid)
+                .ok_or(anyhow::anyhow!("node not found"))?;
+            OspfSectionConfig::validate_as_ref(&config)?;
+            Ok(())
+        }
+
+        pub fn write(&self) -> Result<String, anyhow::Error> {
+            let guard = self.section_config.lock().unwrap().clone();
+            OspfSectionConfig::write_section_config("sdn/fabrics/ospf.cfg", &guard)
+        }
+    }
+}
-- 
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 v2 6/7] perl-rs: sdn: OSPF perlmod methods
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (20 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-perl-rs v2 5/7] perl-rs: sdn: add CRUD helpers for OSPF fabric management Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-perl-rs v2 7/7] perl-rs: sdn: implement OSPF interface file configuration generation Gabriel Goller
                   ` (35 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

CRUD methods for perlmod that call the helper functions defined earlier.
Also add a method that returns the FRR daemons to be enabled and a
method that generates FRR configuration after validating it.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 pve-rs/src/sdn/ospf.rs | 173 ++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 170 insertions(+), 3 deletions(-)

diff --git a/pve-rs/src/sdn/ospf.rs b/pve-rs/src/sdn/ospf.rs
index 4ab1e7e505f0..2c83d638661a 100644
--- a/pve-rs/src/sdn/ospf.rs
+++ b/pve-rs/src/sdn/ospf.rs
@@ -1,14 +1,16 @@
 #[perlmod::package(name = "PVE::RS::SDN::Fabrics::Ospf", lib = "pve_rs")]
 mod export {
-    use std::{net::Ipv4Addr, str};
+    use std::{collections::HashMap, net::Ipv4Addr, str, sync::Mutex};
 
-    use anyhow::Context;
+    use anyhow::{Context, Error};
 
+    use perlmod::Value;
+    use proxmox_frr::serializer::to_raw_config;
     use proxmox_network_types::{debian::Hostname, ip_address::Ipv4Cidr};
     use proxmox_schema::property_string::PropertyString;
     use proxmox_section_config::typed::{ApiSectionDataEntry, SectionConfigData};
     use proxmox_ve_config::sdn::fabric::{
-        FabricId, NodeId, SectionType, Validate,
+        FabricConfig, FabricId, FrrConfigBuilder, NodeId, SectionType, Valid, Validate,
         ospf::{Area, FabricSection, InterfaceProperties, NodeSection, OspfSectionConfig},
     };
     use serde::{Deserialize, Serialize};
@@ -214,4 +216,169 @@ mod export {
             OspfSectionConfig::write_section_config("sdn/fabrics/ospf.cfg", &guard)
         }
     }
+
+    #[export(raw_return)]
+    fn running_config(
+        #[raw] class: Value,
+        raw_config: HashMap<String, OspfSectionConfig>,
+    ) -> Result<perlmod::Value, anyhow::Error> {
+        // we cannot just construct it from the HashMap via From, since then the order is empty
+        let section_config = raw_config.into_iter().collect();
+
+        let return_value = PerlSectionConfig {
+            section_config: Mutex::new(section_config),
+        };
+
+        Ok(perlmod::instantiate_magic!(&class, MAGIC => Box::new(
+                return_value
+        )))
+    }
+
+    #[export(raw_return)]
+    fn config(#[raw] class: Value, raw_config: &[u8]) -> Result<perlmod::Value, anyhow::Error> {
+        let raw_config = std::str::from_utf8(raw_config)?;
+
+        let config = OspfSectionConfig::parse_section_config("ospf.cfg", raw_config)?;
+        let return_value = PerlSectionConfig {
+            section_config: Mutex::new(config),
+        };
+
+        Ok(perlmod::instantiate_magic!(&class, MAGIC => Box::new(
+                return_value
+        )))
+    }
+
+    /// Writes the config to a string and returns the configuration and the protocol.
+    #[export]
+    fn write(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+    ) -> Result<(String, String), Error> {
+        let full_new_config = this.write()?;
+
+        // We return the protocol here as well, so that in perl we can write to
+        // the correct config file
+        Ok((full_new_config, "ospf".to_string()))
+    }
+
+    #[export]
+    fn add_fabric(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+        params: AddFabric,
+    ) -> Result<(), Error> {
+        this.add_fabric(params)?;
+
+        Ok(())
+    }
+
+    #[export]
+    fn add_node(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+        params: AddNode,
+    ) -> Result<(), Error> {
+        this.add_node(params)
+    }
+
+    #[export]
+    fn edit_fabric(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+        params: EditFabric,
+    ) -> Result<(), Error> {
+        this.edit_fabric(params)
+    }
+
+    #[export]
+    fn edit_node(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+        params: EditNode,
+    ) -> Result<(), Error> {
+        this.edit_node(params)
+    }
+
+    #[export]
+    fn delete_fabric(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+        params: DeleteFabric,
+    ) -> Result<(), Error> {
+        this.delete_fabric(params)?;
+
+        Ok(())
+    }
+
+    #[export]
+    fn delete_node(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+        params: DeleteNode,
+    ) -> Result<(), Error> {
+        this.delete_node(params)?;
+
+        Ok(())
+    }
+
+    #[export]
+    fn get_inner(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+    ) -> HashMap<String, OspfSectionConfig> {
+        let guard = this.section_config.lock().unwrap();
+        guard.clone().into_iter().collect()
+    }
+
+    #[export]
+    fn get_fabric(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+        fabric: FabricId,
+    ) -> Result<OspfSectionConfig, Error> {
+        let guard = this.section_config.lock().unwrap();
+        guard
+            .get(&fabric.to_string())
+            .cloned()
+            .ok_or(anyhow::anyhow!("fabric not found"))
+    }
+
+    #[export]
+    fn get_node(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+        fabric: FabricId,
+        node: Hostname,
+    ) -> Result<OspfSectionConfig, Error> {
+        let guard = this.section_config.lock().unwrap();
+        let nodeid = NodeId::new(fabric, node).to_string();
+        guard
+            .get(&nodeid)
+            .cloned()
+            .ok_or(anyhow::anyhow!("node not found"))
+    }
+
+    #[export]
+    pub fn enabled_daemons(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+        hostname: Hostname,
+    ) -> Vec<String> {
+        let config = this.section_config.lock().unwrap();
+
+        for (_, section) in config.iter() {
+            if let OspfSectionConfig::Node(node) = section {
+                if node.node_id == hostname {
+                    return vec!["ospfd".to_string()];
+                }
+            }
+        }
+
+        Vec::new()
+    }
+
+    #[export]
+    pub fn get_frr_raw_config(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+        node: Hostname,
+    ) -> Result<Vec<String>, Error> {
+        let config = this.section_config.lock().unwrap();
+        let ospf_config: Valid<OspfSectionConfig> = OspfSectionConfig::validate(config.clone())?;
+
+        let config = FabricConfig::with_ospf(ospf_config);
+        let frr_config = FrrConfigBuilder::default()
+            .add_fabrics(config)
+            .build(node)?;
+
+        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 v2 7/7] perl-rs: sdn: implement OSPF interface file configuration generation
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (21 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-perl-rs v2 6/7] perl-rs: sdn: OSPF perlmod methods Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-cluster v2 1/1] cluster: add sdn fabrics config files Gabriel Goller
                   ` (34 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

Add function to generate /etc/network/interfaces configuration for OSPF nodes including:
- Create dummy interfaces for each area with /32 addresses
- Configure IP addresses on physical interfaces
- Enable IP forwarding on all relevant interfaces
- Support both numbered and unnumbered interface configurations

Note that the `ospfd` daemon only supports IPv4 so we only have IPv4
addresses for OSPF. In a follow-up we could also support the `ospf6d`
daemon, which supports IPv6.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 pve-rs/src/sdn/ospf.rs | 56 +++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 55 insertions(+), 1 deletion(-)

diff --git a/pve-rs/src/sdn/ospf.rs b/pve-rs/src/sdn/ospf.rs
index 2c83d638661a..744e663ac2ac 100644
--- a/pve-rs/src/sdn/ospf.rs
+++ b/pve-rs/src/sdn/ospf.rs
@@ -1,6 +1,6 @@
 #[perlmod::package(name = "PVE::RS::SDN::Fabrics::Ospf", lib = "pve_rs")]
 mod export {
-    use std::{collections::HashMap, net::Ipv4Addr, str, sync::Mutex};
+    use std::{collections::HashMap, fmt::Write, net::Ipv4Addr, str, sync::Mutex};
 
     use anyhow::{Context, Error};
 
@@ -348,6 +348,60 @@ mod export {
             .ok_or(anyhow::anyhow!("node not found"))
     }
 
+    #[export]
+    fn get_interfaces_etc_network_config(
+        #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
+        node: Hostname,
+    ) -> Result<String, Error> {
+        let guard = this.section_config.lock().unwrap();
+        let mut interfaces = String::new();
+
+        guard.iter().try_for_each(|section| {
+            if let OspfSectionConfig::Node(node_section) = section.1 {
+                if node_section.node_id == node {
+                    // create dummy interface for this fabric
+                    writeln!(interfaces)?;
+                    writeln!(interfaces, "auto dummy_{}", node_section.fabric_id)?;
+                    writeln!(
+                        interfaces,
+                        "iface dummy_{} inet static",
+                        node_section.fabric_id
+                    )?;
+                    writeln!(interfaces, "\tlink-type dummy")?;
+                    writeln!(interfaces, "\tip-forward 1")?;
+                    // add dummy interface address as /32
+                    writeln!(interfaces, "\taddress {}/32", node_section.router_id)?;
+
+                    // add ip-addrs to all other interfaces and ensure they exist
+                    // also enable ip-forwarding on all interfaces as this is needed for unnumbered
+                    // peering
+                    node_section
+                        .clone()
+                        .interfaces
+                        .into_iter()
+                        .try_for_each(|i| {
+                            let interface_name = i.name.clone();
+                            writeln!(interfaces)?;
+                            writeln!(interfaces, "auto {interface_name}")?;
+                            if let Some(ip) = i.ip.map(|i| i.to_string()) {
+                                writeln!(interfaces, "iface {interface_name} inet static")?;
+                                writeln!(interfaces, "\taddress {}", ip)?;
+                                writeln!(interfaces, "\tip-forward 1")?;
+                            } else {
+                                // unnumbered interface needs ip addresses configured in ospf
+                                writeln!(interfaces, "iface {interface_name} inet static")?;
+                                writeln!(interfaces, "\taddress {}/32", node_section.router_id)?;
+                                writeln!(interfaces, "\tip-forward 1")?;
+                            }
+                            Ok::<(), std::fmt::Error>(())
+                        })?;
+                }
+            }
+            Ok::<(), std::fmt::Error>(())
+        })?;
+        Ok(interfaces)
+    }
+
     #[export]
     pub fn enabled_daemons(
         #[try_from_ref] this: &PerlSectionConfig<OspfSectionConfig>,
-- 
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 v2 1/1] cluster: add sdn fabrics config files
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (22 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-perl-rs v2 7/7] perl-rs: sdn: implement OSPF interface file configuration generation Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 17:03   ` [pve-devel] applied: " Thomas Lamprecht
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-access-control v2 1/1] permissions: add ACL paths for SDN fabrics Gabriel Goller
                   ` (33 subsequent siblings)
  57 siblings, 1 reply; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

Add the sdn fabrics config files. These are split into two, as we
currently support two fabric types: ospf and openfabric. They hold the
whole configuration for the respective protocols. They are read and
written by pve-network.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/Cluster.pm  | 2 ++
 src/pmxcfs/status.c | 2 ++
 2 files changed, 4 insertions(+)

diff --git a/src/PVE/Cluster.pm b/src/PVE/Cluster.pm
index e0e3ee995085..a325b67905b8 100644
--- a/src/PVE/Cluster.pm
+++ b/src/PVE/Cluster.pm
@@ -81,6 +81,8 @@ 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/.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 ff5fcc4301e3..5844928d50b6 100644
--- a/src/pmxcfs/status.c
+++ b/src/pmxcfs/status.c
@@ -110,6 +110,8 @@ 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/.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 v2 1/1] permissions: add ACL paths for SDN fabrics
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (23 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-cluster v2 1/1] cluster: add sdn fabrics config files Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 17:20   ` Thomas Lamprecht
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 01/19] sdn: fix value returned by pending_config Gabriel Goller
                   ` (32 subsequent siblings)
  57 siblings, 1 reply; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@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 47f2d38b09c7..7b2dae35448d 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/(openfabric|ospf)/[[: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 v2 01/19] sdn: fix value returned by pending_config
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (24 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-access-control v2 1/1] permissions: add ACL paths for SDN fabrics Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 02/19] debian: add dependency to proxmox-perl-rs Gabriel Goller
                   ` (31 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

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>
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@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 68f9e0fffbfe..c9c45b1c07ea 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 v2 02/19] debian: add dependency to proxmox-perl-rs
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (25 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 01/19] sdn: fix value returned by pending_config Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 03/19] fabrics: add fabrics module Gabriel Goller
                   ` (30 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

We call perlmod rust functions directly from pve-network.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 debian/control | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/debian/control b/debian/control
index 34b736676766..bfa81566a0e4 100644
--- a/debian/control
+++ b/debian/control
@@ -6,11 +6,13 @@ Build-Depends: debhelper-compat (= 13),
                lintian,
                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-rs-perl,
                libpve-access-control,
 Standards-Version: 4.6.1
 Homepage: https://www.proxmox.com
@@ -23,6 +25,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 v2 03/19] fabrics: add fabrics module
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (26 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 02/19] debian: add dependency to proxmox-perl-rs Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 04/19] refactor: controller: move frr methods into helper Gabriel Goller
                   ` (29 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

This module adds the basic functionality required for the sdn fabrics
feature. It includes helpers for reading and writing the configuration
files via perlmod.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/Network/SDN/Fabrics.pm | 87 ++++++++++++++++++++++++++++++++++
 src/PVE/Network/SDN/Makefile   |  2 +-
 2 files changed, 88 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 000000000000..bc371cd8e3a4
--- /dev/null
+++ b/src/PVE/Network/SDN/Fabrics.pm
@@ -0,0 +1,87 @@
+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::INotify;
+use PVE::RS::SDN::Fabrics;
+use PVE::RS::SDN::Fabrics::Ospf;
+use PVE::RS::SDN::Fabrics::OpenFabric;
+
+cfs_register_file(
+    'sdn/fabrics/openfabric.cfg',
+    \&parse_fabrics_config,
+    \&write_fabrics_config,
+);
+
+cfs_register_file(
+    'sdn/fabrics/ospf.cfg',
+    \&parse_fabrics_config,
+    \&write_fabrics_config,
+);
+
+sub parse_fabrics_config {
+    my ($filename, $raw) = @_;
+
+    $raw = '' if !defined($raw);
+    return $raw;
+}
+
+sub write_fabrics_config {
+    my ($filename, $config) = @_;
+    return $config;
+}
+
+my $FABRIC_MODULES = {
+    openfabric => "PVE::RS::SDN::Fabrics::OpenFabric",
+    ospf => "PVE::RS::SDN::Fabrics::Ospf",
+};
+
+sub get_protocols {
+    return sort keys %$FABRIC_MODULES;
+}
+
+sub config {
+    my ($running) = @_;
+
+    my $configs = {};
+
+    for my $protocol (sort keys %$FABRIC_MODULES) {
+	$configs->{$protocol} = PVE::Network::SDN::Fabrics::config_for_protocol($protocol, $running);
+    }
+
+    return $configs;
+}
+
+sub config_for_protocol {
+    my ($protocol, $running) = @_;
+
+    my $module = $FABRIC_MODULES->{$protocol};
+    die "cannot get fabric config \"$protocol\": not implemented" if !$module;
+
+    if ($running) {
+	my $running_config = PVE::Network::SDN::running_config();
+	# required because if the config hasn't been applied yet once after the
+	# introduction of fabrics then the keys do not exist in the running
+	# config so we default to an empty hash
+	my $protocol_config = $running_config->{$protocol}->{ids} // {};
+	return $module->running_config($protocol_config);
+    }
+
+    my $section_config = cfs_read_file("sdn/fabrics/$protocol.cfg");
+    return $module->config($section_config);
+}
+
+sub write_config {
+    my ($config) = @_;
+
+    my ($new_config, $protocol) = $config->write();
+
+    # It is safe to use the protocol in the path here as it comes from rust. There
+    # the protocol is stored in an enum so we know it is correct.
+    cfs_write_file("sdn/fabrics/$protocol.cfg", $new_config, 1);
+}
+
+1;
+
diff --git a/src/PVE/Network/SDN/Makefile b/src/PVE/Network/SDN/Makefile
index 3e6e5fb4c6f2..a256642e3044 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 v2 04/19] refactor: controller: move frr methods into helper
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (27 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 03/19] fabrics: add fabrics module Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 05/19] frr: add new helpers for reloading frr configuration Gabriel Goller
                   ` (28 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

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:

frr_config: This is a perl hash, that loosely resembles the structure
of the FRR configuration file.

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.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 289 ---------------
 src/PVE/Network/SDN/Frr.pm                    | 336 ++++++++++++++++++
 src/PVE/Network/SDN/Makefile                  |   2 +-
 3 files changed, 337 insertions(+), 290 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 c245ea29cf90..f9241a097798 100644
--- a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
@@ -356,293 +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;
-    }
-
-    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], outfunc => {}, 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 000000000000..20eb4d3e405d
--- /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 a256642e3044..d1ffef9eebe7 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 v2 05/19] frr: add new helpers for reloading frr configuration
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (28 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 04/19] refactor: controller: move frr methods into helper Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 06/19] controllers: implement new api for frr config generation Gabriel Goller
                   ` (27 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

The old FRR configuration helper in the EVPN plugin defined a hash as
outfunc, which led to an error in the run_command invocation:

  Not a CODE reference at /usr/share/perl5/PVE/Tools.pm line 577.

This meant that frr-reload.py was never successfully invoked in the
first place.

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.

Due to completely omitting the outfunc, we now print the log of
frr-reload.py to STDOUT, so they are included in the task log for
debugging purposes.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/Network/SDN/Frr.pm | 62 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 62 insertions(+)

diff --git a/src/PVE/Network/SDN/Frr.pm b/src/PVE/Network/SDN/Frr.pm
index 20eb4d3e405d..155cd591ea40 100644
--- a/src/PVE/Network/SDN/Frr.pm
+++ b/src/PVE/Network/SDN/Frr.pm
@@ -44,6 +44,68 @@ 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;
+    }
+
+    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 v2 06/19] controllers: implement new api for frr config generation
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (29 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 05/19] frr: add new helpers for reloading frr configuration Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 07/19] sdn: add frr config generation helper Gabriel Goller
                   ` (26 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

With the changes to how we handle the frr config generation,
controllers are now no longer responsible for generating 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 passed perl hash, instead of directly writing their own
configuration which is now handled externally by the SDN module.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@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 9e8f3aa12a89..788bfccd3aff 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 53963e5ad7f4..3b21cada2d8d 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 f9241a097798..bde331fb12c4 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 97c6876db303..ace19aa7ffdc 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 d6ffc5f35c74..26beff3370a8 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 v2 07/19] sdn: add frr config generation helper
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (30 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 06/19] controllers: implement new api for frr config generation Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 08/19] test: isis: add test for standalone configuration Gabriel Goller
                   ` (25 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

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.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/Network/SDN.pm         | 45 ++++++++++++++++++++++++++++++----
 src/PVE/Network/SDN/Fabrics.pm | 15 ++++++++++++
 src/test/run_test_zones.pl     |  9 +++----
 3 files changed, 59 insertions(+), 10 deletions(-)

diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index c9c45b1c07ea..382147f20522 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 bc371cd8e3a4..02cc2254cbb5 100644
--- a/src/PVE/Network/SDN/Fabrics.pm
+++ b/src/PVE/Network/SDN/Fabrics.pm
@@ -73,6 +73,21 @@ sub config_for_protocol {
     return $module->config($section_config);
 }
 
+sub generate_frr_raw_config {
+    my ($fabric_config) = @_;
+
+    my @raw_config = ();
+
+    my $nodename = PVE::INotify::nodename();
+
+    for my $protocol (sort keys %$fabric_config) {
+	my $protocol_config = $fabric_config->{$protocol}->get_frr_raw_config($nodename);
+	push @raw_config, @$protocol_config if @$protocol_config;
+    }
+
+    return \@raw_config;
+}
+
 sub write_config {
     my ($config) = @_;
 
diff --git a/src/test/run_test_zones.pl b/src/test/run_test_zones.pl
index e506bea1a965..4137da6b2687 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 v2 08/19] test: isis: add test for standalone configuration
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (31 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 07/19] sdn: add frr config generation helper Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 09/19] sdn: frr: add daemon status to frr helper Gabriel Goller
                   ` (24 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

With how the config generation worked before, it was not possible to
create a standalone isis 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. Add a test that covers that scenario.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@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 000000000000..5c9bf1adfbae
--- /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 000000000000..edc8ff918531
--- /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 000000000000..41ae25fda5c3
--- /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 000000000000..331051f3a2c9
--- /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 v2 09/19] sdn: frr: add daemon status to frr helper
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (32 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 08/19] test: isis: add test for standalone configuration Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 10/19] sdn: commit fabrics config to running configuration Gabriel Goller
                   ` (23 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

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 only enable daemons which are
required for the configured fabrics. If a daemon is enabled but a
fabric gets deleted, we disable them.

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.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/Network/SDN.pm         | 18 +++++++++-
 src/PVE/Network/SDN/Fabrics.pm | 17 +++++++++
 src/PVE/Network/SDN/Frr.pm     | 64 ++++++++++++++++++++++++++++++++++
 3 files changed, 98 insertions(+), 1 deletion(-)

diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index 382147f20522..14486e94b3c4 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -258,16 +258,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 02cc2254cbb5..7e0ca8f09341 100644
--- a/src/PVE/Network/SDN/Fabrics.pm
+++ b/src/PVE/Network/SDN/Fabrics.pm
@@ -73,6 +73,23 @@ sub config_for_protocol {
     return $module->config($section_config);
 }
 
+sub get_frr_daemon_status {
+    my ($fabric_config) = @_;
+
+    my $daemon_status = {};
+    my $nodename = PVE::INotify::nodename();
+
+    for my $config (values %$fabric_config) {
+	my $enabled_daemons = $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 155cd591ea40..e2041a51ec43 100644
--- a/src/PVE/Network/SDN/Frr.pm
+++ b/src/PVE/Network/SDN/Frr.pm
@@ -106,6 +106,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 v2 10/19] sdn: commit fabrics config to running configuration
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (33 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 09/19] sdn: frr: add daemon status to frr helper Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 11/19] fabrics: generate ifupdown configuration Gabriel Goller
                   ` (22 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

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.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/Network/SDN.pm         | 7 ++++++-
 src/PVE/Network/SDN/Fabrics.pm | 2 +-
 2 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index 14486e94b3c4..bed3f9e5e50d 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -155,13 +155,18 @@ 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_config = 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} };
 
-    $cfg = { version => $version, vnets => $vnets, zones => $zones, controllers => $controllers, subnets => $subnets };
+    for my $id (sort keys %$fabrics_config) {
+	$fabrics_config->{$id} = $fabrics_config->{$id}->get_inner();
+    }
+
+    $cfg = { version => $version, vnets => $vnets, zones => $zones, controllers => $controllers, subnets => $subnets, fabrics => $fabrics_config };
 
     cfs_write_file($running_cfg, $cfg);
 }
diff --git a/src/PVE/Network/SDN/Fabrics.pm b/src/PVE/Network/SDN/Fabrics.pm
index 7e0ca8f09341..c29a38722fab 100644
--- a/src/PVE/Network/SDN/Fabrics.pm
+++ b/src/PVE/Network/SDN/Fabrics.pm
@@ -65,7 +65,7 @@ sub config_for_protocol {
 	# required because if the config hasn't been applied yet once after the
 	# introduction of fabrics then the keys do not exist in the running
 	# config so we default to an empty hash
-	my $protocol_config = $running_config->{$protocol}->{ids} // {};
+	my $protocol_config = $running_config->{fabrics}->{$protocol} // {};
 	return $module->running_config($protocol_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 pve-network v2 11/19] fabrics: generate ifupdown configuration
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (34 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 10/19] sdn: commit fabrics config to running configuration Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 12/19] api: fabrics: add common helpers Gabriel Goller
                   ` (21 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

Currently, the ifupdown 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.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/Network/SDN.pm         | 68 ++++++++++++++++++++++++++++------
 src/PVE/Network/SDN/Fabrics.pm | 14 +++++++
 src/PVE/Network/SDN/Zones.pm   | 10 -----
 src/test/run_test_zones.pl     |  2 +-
 4 files changed, 71 insertions(+), 23 deletions(-)

diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index bed3f9e5e50d..af1550b0ab40 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -219,18 +219,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 c29a38722fab..997e139968d7 100644
--- a/src/PVE/Network/SDN/Fabrics.pm
+++ b/src/PVE/Network/SDN/Fabrics.pm
@@ -105,6 +105,20 @@ 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);
+
+    my $raw_config = "";
+    for my $protocol (sort keys %$fabric_config) {
+	my $protocol_config = $fabric_config->{$protocol};
+	$raw_config .= $protocol_config->get_interfaces_etc_network_config($nodename);
+    }
+
+    return $raw_config;
+}
+
 sub write_config {
     my ($config) = @_;
 
diff --git a/src/PVE/Network/SDN/Zones.pm b/src/PVE/Network/SDN/Zones.pm
index c1c7745f894e..131ca5ea136d 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 4137da6b2687..794cbdb1d12b 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 v2 12/19] api: fabrics: add common helpers
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (35 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 11/19] fabrics: generate ifupdown configuration Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 13/19] api: openfabric: add api endpoints Gabriel Goller
                   ` (20 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

Since the perlmod API for both the openfabric and ospf are the same,
add helpers for all CRUD operations that will be supported by the
openfabric and ospf endpoints, so they can share the same code.

Additionally we define some standard options for common data types
for fabrics, that will be used by the upcoming fabric API modules.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/API2/Network/SDN/Fabrics/Common.pm | 126 +++++++++++++++++++++
 src/PVE/API2/Network/SDN/Fabrics/Makefile  |   9 ++
 2 files changed, 135 insertions(+)
 create mode 100644 src/PVE/API2/Network/SDN/Fabrics/Common.pm
 create mode 100644 src/PVE/API2/Network/SDN/Fabrics/Makefile

diff --git a/src/PVE/API2/Network/SDN/Fabrics/Common.pm b/src/PVE/API2/Network/SDN/Fabrics/Common.pm
new file mode 100644
index 000000000000..ddd57cafd4ce
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Fabrics/Common.pm
@@ -0,0 +1,126 @@
+package PVE::API2::Network::SDN::Fabrics::Common;
+
+use strict;
+use warnings;
+
+use PVE::JSONSchema;
+use PVE::Tools qw(extract_param);
+
+use PVE::Network::SDN::Fabrics;
+
+PVE::JSONSchema::register_format('pve-sdn-fabric-id', sub {
+    my ($id, $noerr) = @_;
+
+    if ($id !~ m/^[a-z][a-z0-9]{1,7}$/i) {
+	return undef if $noerr;
+	die "zone 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-section-type', {
+    description => "Type of configuration entry in an SDN Fabric section config",
+    type => 'string',
+    enum => ['fabric', 'node'],
+});
+
+PVE::JSONSchema::register_standard_option('pve-sdn-fabric-loopback-prefix', {
+    type => 'string',
+    format => 'CIDRv4',
+    description => 'The IP prefix for Loopback IPs',
+});
+
+sub get_fabric {
+    my ($type, $param) = @_;
+
+    my $fabrics = PVE::Network::SDN::Fabrics::config_for_protocol($type);
+
+    my $config = $fabrics->get_fabric($param->{fabric_id});
+    $config->{digest} = Digest::SHA::sha1_hex($config);
+
+    return $config;
+}
+
+sub get_node {
+    my ($type, $param) = @_;
+
+    my $fabrics = PVE::Network::SDN::Fabrics::config_for_protocol($type);
+
+    my $config = $fabrics->get_node($param->{fabric_id}, $param->{node_id});
+    $config->{digest} = Digest::SHA::sha1_hex($config);
+
+    return $config;
+}
+
+sub add_node {
+    my ($type, $param) = @_;
+
+    my $fabrics = PVE::Network::SDN::Fabrics::config_for_protocol($type);
+    $fabrics->add_node($param);
+    PVE::Network::SDN::Fabrics::write_config($fabrics);
+}
+
+sub add_fabric {
+    my ($type, $param) = @_;
+
+    my $fabrics = PVE::Network::SDN::Fabrics::config_for_protocol($type);
+    $fabrics->add_fabric($param);
+    PVE::Network::SDN::Fabrics::write_config($fabrics);
+}
+
+sub edit_fabric {
+    my ($type, $param) = @_;
+
+    my $fabrics = PVE::Network::SDN::Fabrics::config_for_protocol($type);
+    my $config = $fabrics->get_fabric($param->{fabric_id});
+
+    my $digest = extract_param($param, 'digest');
+    PVE::Tools::assert_if_modified($param->{digest}, $digest);
+
+    $fabrics->edit_fabric($param);
+    PVE::Network::SDN::Fabrics::write_config($fabrics);
+}
+
+sub edit_node {
+    my ($type, $param) = @_;
+
+    my $fabrics = PVE::Network::SDN::Fabrics::config_for_protocol($type);
+    my $config = $fabrics->get_node($param->{fabric_id}, $param->{node_id});
+
+    my $digest = extract_param($param, 'digest');
+    PVE::Tools::assert_if_modified($param->{digest}, $digest);
+
+    $fabrics->edit_node($param);
+    PVE::Network::SDN::Fabrics::write_config($fabrics);
+}
+
+sub delete_fabric {
+    my ($type, $param) = @_;
+
+    my $fabrics = PVE::Network::SDN::Fabrics::config_for_protocol($type);
+    $fabrics->delete_fabric($param);
+    PVE::Network::SDN::Fabrics::write_config($fabrics);
+}
+
+sub delete_node {
+    my ($type, $param) = @_;
+
+    my $fabrics = PVE::Network::SDN::Fabrics::config_for_protocol($type);
+    $fabrics->delete_node($param);
+    PVE::Network::SDN::Fabrics::write_config($fabrics);
+}
+
+1;
diff --git a/src/PVE/API2/Network/SDN/Fabrics/Makefile b/src/PVE/API2/Network/SDN/Fabrics/Makefile
new file mode 100644
index 000000000000..d4267b63997c
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Fabrics/Makefile
@@ -0,0 +1,9 @@
+SOURCES=Common.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 v2 13/19] api: openfabric: add api endpoints
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (36 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 12/19] api: fabrics: add common helpers Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 14/19] api: openfabric: add node endpoints Gabriel Goller
                   ` (19 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

Add CRUD endpoints for OpenFabric fabrics. The logic is handled by
proxmox-perl-rs itself, the endpoints are just proxying the Rust
implementation.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/API2/Network/SDN/Fabrics/Makefile     |   2 +-
 .../API2/Network/SDN/Fabrics/OpenFabric.pm    | 125 ++++++++++++++++++
 2 files changed, 126 insertions(+), 1 deletion(-)
 create mode 100644 src/PVE/API2/Network/SDN/Fabrics/OpenFabric.pm

diff --git a/src/PVE/API2/Network/SDN/Fabrics/Makefile b/src/PVE/API2/Network/SDN/Fabrics/Makefile
index d4267b63997c..9caae13ad963 100644
--- a/src/PVE/API2/Network/SDN/Fabrics/Makefile
+++ b/src/PVE/API2/Network/SDN/Fabrics/Makefile
@@ -1,4 +1,4 @@
-SOURCES=Common.pm
+SOURCES=Common.pm OpenFabric.pm
 
 
 PERL5DIR=${DESTDIR}/usr/share/perl5
diff --git a/src/PVE/API2/Network/SDN/Fabrics/OpenFabric.pm b/src/PVE/API2/Network/SDN/Fabrics/OpenFabric.pm
new file mode 100644
index 000000000000..7382f1095d49
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Fabrics/OpenFabric.pm
@@ -0,0 +1,125 @@
+package PVE::API2::Network::SDN::Fabrics::OpenFabric;
+
+use strict;
+use warnings;
+
+use PVE::JSONSchema qw(get_standard_option);
+
+use PVE::API2::Network::SDN::Fabrics::Common;
+use PVE::API2::Network::SDN::Fabrics::OpenFabricNode;
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+my $update_fabric_properties = {
+    digest => get_standard_option('pve-config-digest'),
+    fabric_id => get_standard_option('pve-sdn-fabric-id'),
+    hello_interval => $PVE::API2::Network::SDN::Fabrics::OpenFabricNode::hello_interval_option,
+};
+
+our $fabric_properties = {
+    loopback_prefix => get_standard_option('pve-sdn-fabric-loopback-prefix'),
+    %$update_fabric_properties,
+};
+
+__PACKAGE__->register_method ({
+    subclass => "PVE::API2::Network::SDN::Fabrics::OpenFabricNode",
+    path => '{fabric_id}/node',
+});
+
+__PACKAGE__->register_method({
+    name => 'get_fabric',
+    path => '{fabric_id}',
+    method => 'GET',
+    description => 'Get the configuration for an OpenFabric fabric',
+    permissions => {
+	check => ['perm', '/sdn/fabrics/openfabric/{fabric_id}', [ 'SDN.Audit', 'SDN.Allocate' ], any => 1],
+    },
+    parameters => {
+	properties => {
+	    fabric_id => get_standard_option('pve-sdn-fabric-id'),
+	},
+    },
+    returns => {
+	type => 'object',
+	properties => $fabric_properties,
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::get_fabric("openfabric", $param);
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'add_fabric',
+    path => '/',
+    method => 'POST',
+    description => 'Add an OpenFabric fabric',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/openfabric', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+	properties => $fabric_properties,
+    },
+    returns => { type => 'null' },
+    code => sub {
+	my ($param) = @_;
+
+	PVE::Network::SDN::lock_sdn_config(
+	    sub {
+		PVE::API2::Network::SDN::Fabrics::Common::add_fabric("openfabric", $param);
+	    }, "add sdn fabric failed");
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'update_fabric',
+    path => '{fabric_id}',
+    method => 'PUT',
+    description => 'Update a OpenFabric fabric',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/openfabric/{fabric_id}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+	properties => $update_fabric_properties,
+    },
+    returns => { type => 'null' },
+    code => sub {
+	my ($param) = @_;
+
+	PVE::Network::SDN::lock_sdn_config(
+	    sub {
+		PVE::API2::Network::SDN::Fabrics::Common::edit_fabric("openfabric", $param);
+	    }, "edit sdn fabric failed");
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'delete_fabric',
+    path => '{fabric_id}',
+    method => 'DELETE',
+    description => 'Delete a OpenFabric fabric',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/openfabric/{fabric_id}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+	properties => {
+	    fabric_id => get_standard_option('pve-sdn-fabric-id'),
+	},
+    },
+    returns => { type => 'null' },
+    code => sub {
+	my ($param) = @_;
+
+	PVE::Network::SDN::lock_sdn_config(
+	    sub {
+		PVE::API2::Network::SDN::Fabrics::Common::delete_fabric("openfabric", $param);
+	    }, "delete sdn fabric failed");
+    },
+});
+
+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 v2 14/19] api: openfabric: add node endpoints
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (37 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 13/19] api: openfabric: add api endpoints Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 15/19] api: ospf: add fabric endpoints Gabriel Goller
                   ` (18 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

Add CRUD endpoints for manipulating OpenFabric nodes. They are
implemented in proxmox-perl-rs.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/API2/Network/SDN/Fabrics/Makefile     |   2 +-
 .../Network/SDN/Fabrics/OpenFabricNode.pm     | 181 ++++++++++++++++++
 2 files changed, 182 insertions(+), 1 deletion(-)
 create mode 100644 src/PVE/API2/Network/SDN/Fabrics/OpenFabricNode.pm

diff --git a/src/PVE/API2/Network/SDN/Fabrics/Makefile b/src/PVE/API2/Network/SDN/Fabrics/Makefile
index 9caae13ad963..af733ad013fd 100644
--- a/src/PVE/API2/Network/SDN/Fabrics/Makefile
+++ b/src/PVE/API2/Network/SDN/Fabrics/Makefile
@@ -1,4 +1,4 @@
-SOURCES=Common.pm OpenFabric.pm
+SOURCES=Common.pm OpenFabric.pm OpenFabricNode.pm
 
 
 PERL5DIR=${DESTDIR}/usr/share/perl5
diff --git a/src/PVE/API2/Network/SDN/Fabrics/OpenFabricNode.pm b/src/PVE/API2/Network/SDN/Fabrics/OpenFabricNode.pm
new file mode 100644
index 000000000000..5e4d4befe7ab
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Fabrics/OpenFabricNode.pm
@@ -0,0 +1,181 @@
+package PVE::API2::Network::SDN::Fabrics::OpenFabricNode;
+
+use strict;
+use warnings;
+
+use PVE::JSONSchema qw(get_standard_option);
+
+use PVE::API2::Network::SDN::Fabrics::Common;
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+our $hello_interval_option = {
+    type => 'number',
+    description => 'The hello_interval property of the interface',
+    optional => 1,
+    minimum => 1,
+    maximum => 600,
+};
+
+my $interface_properties = {
+    name => {
+	type => 'string',
+	format => 'pve-iface',
+	description => 'Name of the network interface',
+    },
+    ip => {
+	type => 'string',
+	format => 'CIDRv4',
+	description => 'The IPv4 address of the interface',
+	optional => 1,
+    },
+    ipv6 => {
+	type => 'string',
+	format => 'CIDRv6',
+	description => 'The IPv6 address of the interface',
+	optional => 1,
+    },
+    passive => {
+	type => 'boolean',
+	description => 'The passive property of the interface',
+	optional => 1,
+    },
+    hello_interval => $hello_interval_option,
+    csnp_interval => {
+	type => 'number',
+	description => 'The csnp_interval property of the interface',
+	optional => 1,
+	minimum => 1,
+	maximum => 600,
+    },
+    hello_multiplier => {
+	type => 'number',
+	description => 'The hello_multiplier property of the interface',
+	optional => 1,
+	minimum => 2,
+	maximum => 600,
+    },
+};
+
+my $node_properties = {
+    digest => get_standard_option('pve-config-digest'),
+    fabric_id => get_standard_option('pve-sdn-fabric-id'),
+    node_id => get_standard_option('pve-sdn-fabric-node-id'),
+    router_id => {
+	type => 'string',
+	format => 'ip',
+	description => 'The Router-ID of this node (will be converted to a real NET later)',
+    },
+    interfaces => {
+	type => 'array',
+	optional => 1,
+	description => 'List of interfaces on this node that are part of the fabric.',
+	items => {
+	    type => 'string',
+	    description => 'OpenFabric interface configuration.',
+	    format => $interface_properties,
+	},
+    },
+};
+
+__PACKAGE__->register_method({
+    name => 'get_node',
+    path => '{node_id}',
+    method => 'GET',
+    description => 'Get the configuration for a node in an OpenFabric fabric',
+    permissions => {
+	check => ['perm', '/sdn/fabrics/openfabric/{fabric_id}', [ 'SDN.Audit', 'SDN.Allocate' ], any => 1],
+    },
+    parameters => {
+	properties => {
+	    fabric_id => get_standard_option('pve-sdn-fabric-id'),
+	    node_id => get_standard_option('pve-sdn-fabric-node-id'),
+	},
+    },
+    returns => {
+	type => 'object',
+	properties => $node_properties,
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::get_node("openfabric", $param);
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'add_node',
+    path => '/',
+    method => 'POST',
+    description => 'Add a node to an OpenFabric fabric',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/openfabric/{fabric_id}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+	properties => $node_properties,
+    },
+    returns => { type => 'null' },
+    code => sub {
+	my ($param) = @_;
+
+	PVE::Network::SDN::lock_sdn_config(
+	    sub {
+		PVE::API2::Network::SDN::Fabrics::Common::add_node("openfabric", $param);
+	    }, "add sdn fabric node failed");
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'update_node',
+    path => '{node_id}',
+    method => 'PUT',
+    description => 'Update a node in an OpenFabric fabric',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/openfabric/{fabric_id}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+	properties => $node_properties,
+    },
+    returns => { type => 'null' },
+    code => sub {
+	my ($param) = @_;
+
+	PVE::Network::SDN::lock_sdn_config(
+	    sub {
+		PVE::API2::Network::SDN::Fabrics::Common::edit_node("openfabric", $param);
+	    }, "edit sdn fabric node failed");
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'delete_node',
+    path => '{node_id}',
+    method => 'DELETE',
+    description => 'Delete a node from an OpenFabric fabric',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/openfabric/{fabric_id}', [ 'SDN.Allocate' ]],
+    },
+    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 {
+		PVE::API2::Network::SDN::Fabrics::Common::delete_node("openfabric", $param);
+	    }, "delete sdn fabric node failed");
+    },
+});
+
+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 v2 15/19] api: ospf: add fabric endpoints
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (38 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 14/19] api: openfabric: add node endpoints Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 16/19] api: ospf: add node endpoints Gabriel Goller
                   ` (17 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

Add CRUD endpoints for the ospf fabric and node section types. They
are implemented in proxmox-perl-rs.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/API2/Network/SDN/Fabrics/Makefile |   2 +-
 src/PVE/API2/Network/SDN/Fabrics/OSPF.pm  | 155 ++++++++++++++++++++++
 2 files changed, 156 insertions(+), 1 deletion(-)
 create mode 100644 src/PVE/API2/Network/SDN/Fabrics/OSPF.pm

diff --git a/src/PVE/API2/Network/SDN/Fabrics/Makefile b/src/PVE/API2/Network/SDN/Fabrics/Makefile
index af733ad013fd..62d794486ebe 100644
--- a/src/PVE/API2/Network/SDN/Fabrics/Makefile
+++ b/src/PVE/API2/Network/SDN/Fabrics/Makefile
@@ -1,4 +1,4 @@
-SOURCES=Common.pm OpenFabric.pm OpenFabricNode.pm
+SOURCES=Common.pm OpenFabric.pm OpenFabricNode.pm OSPF.pm
 
 
 PERL5DIR=${DESTDIR}/usr/share/perl5
diff --git a/src/PVE/API2/Network/SDN/Fabrics/OSPF.pm b/src/PVE/API2/Network/SDN/Fabrics/OSPF.pm
new file mode 100644
index 000000000000..bb32e785aaff
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Fabrics/OSPF.pm
@@ -0,0 +1,155 @@
+package PVE::API2::Network::SDN::Fabrics::OSPF;
+
+use strict;
+use warnings;
+
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::Tools qw(extract_param $IPV4RE);
+
+use PVE::API2::Network::SDN::Fabrics::Common;
+use PVE::API2::Network::SDN::Fabrics::OSPFNode;
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+# either a 32-bit unsigned integer or an IP address
+sub parse_ospf_area {
+    my ($id, $noerr) = @_;
+
+    if ($id =~ m/\d+/) {
+	if ($id < 0 || $id > 4294967295) {
+	    return undef if $noerr;
+	    die "$id is a number, but outside of the valid 32-bit unsigned integer range";
+	}
+    } elsif ($id !~ m/^$IPV4RE$/) {
+	return undef if $noerr;
+	die "$id is not a valid IPv4 address nor a number";
+    }
+
+    return $id;
+}
+
+PVE::JSONSchema::register_format('pve-sdn-fabric-ospf-area', \&parse_ospf_area);
+
+my $update_fabric_properties = {
+    digest => get_standard_option('pve-config-digest'),
+    area => {
+	type => 'string',
+	description => 'OSPF Area (either a 32-bit unsigned integer or IPv4 address)',
+	format => 'pve-sdn-fabric-ospf-area',
+    },
+};
+
+our $fabric_properties = {
+    fabric_id => get_standard_option('pve-sdn-fabric-id'),
+    loopback_prefix => get_standard_option('pve-sdn-fabric-loopback-prefix'),
+    %$update_fabric_properties,
+};
+
+__PACKAGE__->register_method ({
+    subclass => "PVE::API2::Network::SDN::Fabrics::OSPFNode",
+    path => '{fabric_id}/node',
+});
+
+__PACKAGE__->register_method({
+    name => 'get_fabric',
+    path => '{fabric_id}',
+    method => 'GET',
+    description => 'Get the configuration for an OSPF fabric',
+    permissions => {
+	check => ['perm', '/sdn/fabrics/ospf/{fabric_id}', [ 'SDN.Audit', 'SDN.Allocate' ], any => 1],
+    },
+    parameters => {
+	properties => {
+	    fabric_id => get_standard_option('pve-sdn-fabric-id'),
+	},
+    },
+    returns => {
+	type => 'object',
+	properties => $fabric_properties,
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::get_fabric("ospf", $param);
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'add_fabric',
+    path => '/',
+    method => 'POST',
+    description => 'Add an OSPF fabric',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/ospf', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+	properties => $fabric_properties,
+    },
+    returns => {
+	type => 'null',
+    },
+    code => sub {
+	my ($param) = @_;
+
+	PVE::Network::SDN::lock_sdn_config(
+	    sub {
+		PVE::API2::Network::SDN::Fabrics::Common::add_fabric("ospf", $param);
+	    }, "add sdn fabric failed");
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'update_fabric',
+    path => '{fabric_id}',
+    method => 'PUT',
+    description => 'Update a OSPF fabric',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/ospf/{fabric_id}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+	properties => $update_fabric_properties,
+    },
+    returns => {
+	type => 'null',
+    },
+    code => sub {
+	my ($param) = @_;
+
+	PVE::Network::SDN::lock_sdn_config(
+	    sub {
+		PVE::API2::Network::SDN::Fabrics::Common::edit_fabric("ospf", $param);
+	    }, "edit sdn fabric failed");
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'delete_fabric',
+    path => '{fabric_id}',
+    method => 'DELETE',
+    description => 'Delete a OSPF fabric',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/ospf/{fabric_id}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+	properties => {
+	    fabric_id => get_standard_option('pve-sdn-fabric-id'),
+	},
+    },
+    returns => {
+	type => 'null',
+    },
+    code => sub {
+	my ($param) = @_;
+
+	PVE::Network::SDN::lock_sdn_config(
+	    sub {
+		PVE::API2::Network::SDN::Fabrics::Common::delete_fabric("ospf", $param);
+	    }, "delete sdn fabric failed");
+    },
+});
+
+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 v2 16/19] api: ospf: add node endpoints
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (39 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 15/19] api: ospf: add fabric endpoints Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 17/19] api: fabrics: add module / subfolder Gabriel Goller
                   ` (16 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

Add CRUD endpoints for OSPF nodes. They are implemented in
proxmox-perl-rs.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/API2/Network/SDN/Fabrics/Makefile    |   2 +-
 src/PVE/API2/Network/SDN/Fabrics/OSPFNode.pm | 163 +++++++++++++++++++
 2 files changed, 164 insertions(+), 1 deletion(-)
 create mode 100644 src/PVE/API2/Network/SDN/Fabrics/OSPFNode.pm

diff --git a/src/PVE/API2/Network/SDN/Fabrics/Makefile b/src/PVE/API2/Network/SDN/Fabrics/Makefile
index 62d794486ebe..5d529add017d 100644
--- a/src/PVE/API2/Network/SDN/Fabrics/Makefile
+++ b/src/PVE/API2/Network/SDN/Fabrics/Makefile
@@ -1,4 +1,4 @@
-SOURCES=Common.pm OpenFabric.pm OpenFabricNode.pm OSPF.pm
+SOURCES=Common.pm OpenFabric.pm OpenFabricNode.pm OSPF.pm OSPFNode.pm
 
 
 PERL5DIR=${DESTDIR}/usr/share/perl5
diff --git a/src/PVE/API2/Network/SDN/Fabrics/OSPFNode.pm b/src/PVE/API2/Network/SDN/Fabrics/OSPFNode.pm
new file mode 100644
index 000000000000..d256bc84d438
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Fabrics/OSPFNode.pm
@@ -0,0 +1,163 @@
+package PVE::API2::Network::SDN::Fabrics::OSPFNode;
+
+use strict;
+use warnings;
+
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::Tools qw(extract_param);
+
+use PVE::API2::Network::SDN::Fabrics::Common;
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+my $interface_format = {
+    name => {
+	type => 'string',
+	format => 'pve-iface',
+	description => 'Name of the interface',
+    },
+    passive => {
+	type => 'boolean',
+	description => 'The passive property of the interface',
+	optional => 1,
+    },
+    ip => {
+	type => 'string',
+	format => 'CIDRv4',
+	description => 'The IPv4 address of the interface',
+	optional => 1,
+    },
+    unnumbered => {
+	type => 'boolean',
+	description => 'If the interface is unnumbered',
+	optional => 1,
+    },
+};
+
+our $node_properties = {
+    digest => get_standard_option('pve-config-digest'),
+    fabric_id => get_standard_option('pve-sdn-fabric-id'),
+    node_id => get_standard_option('pve-sdn-fabric-node-id'),
+    router_id => {
+	type => 'string',
+	format => 'ipv4',
+	description => 'The Router-ID of this node',
+    },
+    interfaces => {
+	type => 'array',
+	optional => 1,
+	description => 'Array of openfabric interfaces as propertystrings',
+	items => {
+	    type => 'string',
+	    description => 'Propertystring of openfabric interfaces',
+	    format => $interface_format,
+	},
+    },
+};
+
+__PACKAGE__->register_method({
+    name => 'get_node',
+    path => '{node_id}',
+    method => 'GET',
+    description => 'Get the configuration for a node in an OSPF fabric',
+    permissions => {
+	check => ['perm', '/sdn/fabrics/ospf/{fabric_id}', [ 'SDN.Audit', 'SDN.Allocate' ], any => 1],
+    },
+    parameters => {
+	properties => {
+	    fabric_id => get_standard_option('pve-sdn-fabric-id'),
+	    node_id => get_standard_option('pve-sdn-fabric-node-id'),
+	},
+    },
+    returns => {
+	type => 'object',
+	properties => $node_properties,
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return PVE::API2::Network::SDN::Fabrics::Common::get_node("ospf", $param);
+    },
+});
+
+
+__PACKAGE__->register_method({
+    name => 'add_node',
+    path => '/',
+    method => 'POST',
+    description => 'Add a node to an OSPF fabric',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/ospf/{fabric_id}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+	properties => $node_properties,
+    },
+    returns => {
+	type => 'null',
+    },
+    code => sub {
+	my ($param) = @_;
+
+	PVE::Network::SDN::lock_sdn_config(
+	    sub {
+		PVE::API2::Network::SDN::Fabrics::Common::add_node("ospf", $param);
+	    }, "add sdn fabric node failed");
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'update_node',
+    path => '{node_id}',
+    method => 'PUT',
+    description => 'Update a node in an OSPF fabric',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/ospf/{fabric_id}', [ 'SDN.Allocate' ]],
+    },
+    parameters => {
+	properties => $node_properties,
+    },
+    returns => {
+	type => 'null',
+    },
+    code => sub {
+	my ($param) = @_;
+
+	PVE::Network::SDN::lock_sdn_config(
+	    sub {
+		PVE::API2::Network::SDN::Fabrics::Common::edit_node("ospf", $param);
+	    }, "edit sdn fabric node failed");
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'delete_node',
+    path => '{node_id}',
+    method => 'DELETE',
+    description => 'Delete a node from an OSPF fabric',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/sdn/fabrics/ospf/{fabric_id}', [ 'SDN.Allocate' ]],
+    },
+    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 {
+		PVE::API2::Network::SDN::Fabrics::Common::delete_node("ospf", $param);
+	    }, "delete sdn fabric node failed");
+    },
+});
+
+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 v2 17/19] api: fabrics: add module / subfolder
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (40 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 16/19] api: ospf: add node endpoints Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 18/19] test: fabrics: add test cases for ospf and openfabric + evpn Gabriel Goller
                   ` (15 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

Add a new subfolder that hosts all the API methods for the sdn
fabrics. We also add a method for listing all fabrics of all types as
a GET endpoint, with the respective schemas. It supports the same
filtering options as the other SDN GET endpoints (pending / running).

We also need to add a special case in encode_value for the interfaces
key of nodes, since they require special handling when encoding
because they are arrays.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 src/PVE/API2/Network/SDN.pm         |   7 +
 src/PVE/API2/Network/SDN/Fabrics.pm | 208 ++++++++++++++++++++++++++++
 src/PVE/API2/Network/SDN/Makefile   |   3 +-
 src/PVE/Network/SDN.pm              |   2 +-
 4 files changed, 218 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 d216e4878b61..ccbf0777e3d4 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 000000000000..86785ee47cff
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/Fabrics.pm
@@ -0,0 +1,208 @@
+package PVE::API2::Network::SDN::Fabrics;
+
+use strict;
+use warnings;
+
+use Storable qw(dclone);
+
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::RPCEnvironment;
+use PVE::Tools qw(extract_param);
+
+use PVE::Network::SDN::Fabrics;
+
+use PVE::API2::Network::SDN::Fabrics::OpenFabric;
+use PVE::API2::Network::SDN::Fabrics::OpenFabricNode;
+use PVE::API2::Network::SDN::Fabrics::OSPF;
+use PVE::API2::Network::SDN::Fabrics::OSPFNode;
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method ({
+    subclass => "PVE::API2::Network::SDN::Fabrics::OpenFabric",
+    path => 'openfabric',
+});
+
+__PACKAGE__->register_method ({
+    subclass => "PVE::API2::Network::SDN::Fabrics::OSPF",
+    path => 'ospf',
+});
+
+__PACKAGE__->register_method ({
+    name => 'index',
+    path => '',
+    method => 'GET',
+    permissions => { user => 'all' },
+    description => "SDN Fabrics Index",
+    parameters => {
+	additionalProperties => 0,
+	properties => {},
+    },
+    returns => {
+	type => 'array',
+	items => {
+	    type => "object",
+	    properties => {},
+	},
+	links => [ { rel => 'child', href => "{protocol}" } ],
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return [
+	    { protocol => 'openfabric' },
+	    { protocol => 'ospf' },
+	];
+    }});
+
+__PACKAGE__->register_method({
+    name => 'fabric_index',
+    path => 'all',
+    method => 'GET',
+    description => 'Index of SDN Fabrics',
+    permissions => {
+	description => "Only list entries where you have 'SDN.Audit' or 'SDN.Allocate' permissions on '/sdn/fabrics/<protocol>/<fabric>'",
+	user => 'all',
+    },
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    running => {
+		type => 'boolean',
+		optional => 1,
+		description => "Display running config.",
+	    },
+	    pending => {
+		type => 'boolean',
+		optional => 1,
+		description => "Display pending config.",
+	    },
+	},
+    },
+    returns => {
+	type => 'object',
+	properties => {
+	    openfabric => {
+		type => 'array',
+		items => {
+		    type => 'object',
+		    properties => {
+			'type' => get_standard_option('pve-sdn-fabric-section-type'),
+			'config' => {
+			    type => 'object',
+			    'type-property' => 'type',
+			    oneOf => [
+				{
+				    'instance-types' => ['fabric'],
+				    type => 'object',
+				    description => 'OpenFabric fabric',
+				    properties => $PVE::API2::Network::SDN::Fabrics::OpenFabric::fabric_properties,
+				},
+				{
+				    'instance-types' => ['node'],
+				    type => 'object',
+				    description => 'OpenFabric node',
+				    properties => $PVE::API2::Network::SDN::Fabrics::OpenFabricNode::node_properties,
+				},
+			    ],
+			},
+		    },
+		},
+	    },
+	    ospf => {
+		type => 'array',
+		items => {
+		    type => 'object',
+		    properties => {
+			'type' => get_standard_option('pve-sdn-fabric-section-type'),
+			config => {
+			    type => 'object',
+			    'type-property' => 'type',
+			    oneOf => [
+				{
+				    'instance-types' => ['fabric'],
+				    type => 'object',
+				    description => 'OSPF fabric',
+				    properties => $PVE::API2::Network::SDN::Fabrics::OSPF::fabric_properties,
+				},
+				{
+				    'instance-types' => ['node'],
+				    type => 'object',
+				    description => 'OSPF node',
+				    properties => $PVE::API2::Network::SDN::Fabrics::OSPFNode::node_properties,
+				},
+			    ]
+			},
+		    },
+		},
+	    },
+	},
+    },
+    code => sub {
+	my ($param) = @_;
+
+	my $rpcenv = PVE::RPCEnvironment::get();
+	my $authuser = $rpcenv->get_user();
+	my $privs = [ 'SDN.Audit', 'SDN.Allocate' ];
+
+	my $running = extract_param($param, 'running');
+	my $pending = extract_param($param, 'pending');
+
+	my $extract_fabric_id = sub {
+	    my ($entry) = @_;
+
+	    my $data = $entry;
+
+	    if ($entry->{state} && $entry->{state} eq 'new') {
+		$data = $entry->{pending};
+	    }
+
+	    return $data->{fabric_id};
+	};
+
+	my $res = {};
+
+	my @protocols = PVE::Network::SDN::Fabrics::get_protocols();
+	foreach my $protocol (@protocols) {
+	    $res->{$protocol} = [];
+
+	    my $config;
+
+	    if ($pending) {
+		my $section_config = PVE::Network::SDN::Fabrics::config_for_protocol($protocol, 0)
+		    ->get_inner();
+		my $running_config = PVE::Network::SDN::Fabrics::config_for_protocol($protocol, 1)
+		    ->get_inner();
+
+		# pending_config expects the configuration to be under the ids
+		# key, but the Fabrics function doesn't include that key
+		$config = PVE::Network::SDN::pending_config(
+		    { $protocol => { ids => $running_config } },
+		    { ids => $section_config },
+		    $protocol
+		);
+
+		$config = $config->{ids};
+	    } elsif ($running) {
+		$config = PVE::Network::SDN::Fabrics::config_for_protocol($protocol, 1)
+		    ->get_inner();
+	    } else {
+		$config = PVE::Network::SDN::Fabrics::config_for_protocol($protocol, 0)
+		    ->get_inner();
+	    }
+
+	    foreach my $id (sort keys %$config) {
+		my $entry = $config->{$id};
+
+		my $fabric_id = $extract_fabric_id->($entry);
+		next if !$rpcenv->check_any($authuser, "/sdn/fabrics/$protocol/$fabric_id", $privs, 1);
+
+		push @{$res->{$protocol}}, dclone($entry);
+	    }
+	}
+	return $res;
+    },
+});
+
+1;
diff --git a/src/PVE/API2/Network/SDN/Makefile b/src/PVE/API2/Network/SDN/Makefile
index abd1bfae020e..08bec7535530 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 af1550b0ab40..6057bca1c6b1 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -344,7 +344,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 v2 18/19] test: fabrics: add test cases for ospf and openfabric + evpn
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (41 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 17/19] api: fabrics: add module / subfolder Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 19/19] frr: bump frr config version to 10.2.1 Gabriel Goller
                   ` (14 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

Add two additional test cases for EVPN zones, which use fabrics as the
underlay network - one for OSPF, one for OpenFabric.

They cover a full-mesh fabric setup as well as a simple point-to-point
setup to a route reflector as the underlay fabric.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 .../expected_controller_config                | 72 ++++++++++++++++
 .../openfabric_fabric/expected_sdn_interfaces | 56 ++++++++++++
 .../zones/evpn/openfabric_fabric/interfaces   |  6 ++
 .../zones/evpn/openfabric_fabric/sdn_config   | 85 +++++++++++++++++++
 .../ospf_fabric/expected_controller_config    | 66 ++++++++++++++
 .../evpn/ospf_fabric/expected_sdn_interfaces  | 53 ++++++++++++
 src/test/zones/evpn/ospf_fabric/interfaces    |  6 ++
 src/test/zones/evpn/ospf_fabric/sdn_config    | 82 ++++++++++++++++++
 8 files changed, 426 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 000000000000..264a7c422c11
--- /dev/null
+++ b/src/test/zones/evpn/openfabric_fabric/expected_controller_config
@@ -0,0 +1,72 @@
+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 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 openfabric_test_ips permit 172.20.3.0/24
+!
+route-map openfabric permit 100
+ match ip address openfabric_test_ips
+ set src 172.20.3.1
+exit
+!
+ip protocol openfabric route-map 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 000000000000..e76e0b9e00d5
--- /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
+	link-type dummy
+	ip-forward 1
+	address 172.20.3.1/32
+
+auto ens20
+iface ens20 inet manual
+	ip-forward 1
+
+auto ens21
+iface ens21 inet manual
+	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 000000000000..1b4384bdd8a3
--- /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 000000000000..f824aeef28ac
--- /dev/null
+++ b/src/test/zones/evpn/openfabric_fabric/sdn_config
@@ -0,0 +1,85 @@
+{
+          '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' => {
+                                                    'peers' => '172.20.3.1,172.20.3.2,172.20.3.3',
+                                                    'asn' => 65000,
+                                                    'type' => 'evpn'
+                                                  }
+                                      }
+                           },
+           'fabrics' => {
+                 'openfabric' => {
+                               'test' => {
+                                           'type' => 'fabric',
+                                           'fabric_id' => 'test',
+                                           'hello_interval' => 1,
+                                           'loopback_prefix' => '172.20.3.0/24',
+                                         },
+                               'test_localhost' => {
+                                                   'interfaces' => [
+                                                                    'name=ens20',
+                                                                    'name=ens21'
+                                                                  ],
+                                                   'id' => 'test_localhost',
+                                                   'node_id' => 'localhost',
+                                                   'fabric_id' => 'test',
+                                                   'type' => 'node',
+                                                   'router_id' => '172.20.3.1',
+                                                 },
+                               'test_pathfinder' => {
+                                                      'id' => 'test_pathfinder',
+                                                      'node_id' => 'pathfinder',
+                                                      'fabric_id' => 'test',
+                                                      'interfaces' => [
+                                                                       'name=ens20',
+                                                                       'name=ens21'
+                                                                     ],
+                                                      'router_id' => '172.20.3.2',
+                                                      'type' => 'node',
+                                                    },
+                               'test_raider' => {
+                                                  'router_id' => '172.20.3.3',
+                                                  'type' => 'node',
+                                                  'interfaces' => [
+                                                                   'name=ens21',
+                                                                   'name=ens20'
+                                                                 ],
+                                                  'id' => 'test_raider',
+                                                  'node_id' => 'raider',
+                                                  'fabric_id' => 'test',
+                                                }
+                                     }
+              }
+        };
+
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 000000000000..4e8e0c12deda
--- /dev/null
+++ b/src/test/zones/evpn/ospf_fabric/expected_controller_config
@@ -0,0 +1,66 @@
+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 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 ospf_test_ips permit 172.20.30.0/24
+!
+route-map ospf permit 100
+ match ip address ospf_test_ips
+ set src 172.20.30.1
+exit
+!
+ip protocol ospf route-map 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 000000000000..56cda622f8ba
--- /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
+	link-type dummy
+	ip-forward 1
+	address 172.20.30.1/32
+
+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 000000000000..79ba9c11bd0a
--- /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 000000000000..eb3a50a56960
--- /dev/null
+++ b/src/test/zones/evpn/ospf_fabric/sdn_config
@@ -0,0 +1,82 @@
+{
+          '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' => {
+                                                    'peers' => '172.20.30.1,172.20.30.2,172.20.30.3',
+                                                    'asn' => 65000,
+                                                    'type' => 'evpn'
+                                                  }
+                                      }
+                           },
+           'fabrics' => {
+                  'ospf' => {
+                                 'test_pathfinder' => {
+                                                     'id' => 'test_pathfinder',
+                                                     'fabric_id' => 'test',
+                                                     'node_id' => 'pathfinder',
+                                                     'interfaces' => [
+                                                                      'name=ens19,ip=172.16.3.20/31'
+                                                                    ],
+                                                     'router_id' => '172.20.30.2',
+                                                     'type' => 'node'
+                                                   },
+                                 'test' => {
+                                          'loopback_prefix' => '172.20.30.0/24',
+                                          'area' => '0',
+                                          'type' => 'fabric',
+                                          'fabric_id' => 'test',
+                                        },
+                                 'test_localhost' => {
+                                                  'id' => 'test_localhost',
+                                                  'fabric_id' => 'test',
+                                                  'node_id' => 'localhost',
+                                                  'interfaces' => [
+                                                                   'name=ens19,passive=false,ip=172.16.3.10/31'
+                                                                 ],
+                                                  'router_id' => '172.20.30.1',
+                                                  'type' => 'node'
+                                                },
+                                 'test_raider' => {
+                                                 'type' => 'node',
+                                                 'router_id' => '172.20.30.3',
+                                                 'id' => 'test_raider',
+                                                 'fabric_id' => 'test',
+                                                 'node_id' => '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 v2 19/19] frr: bump frr config version to 10.2.1
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (42 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 18/19] test: fabrics: add test cases for ospf and openfabric + evpn Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-manager v2 01/11] api: use new generalized frr and etc network config helper functions Gabriel Goller
                   ` (13 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

With the package bumped to 10.2.1 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.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@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 e2041a51ec43..ebbcac2aae90 100644
--- a/src/PVE/Network/SDN/Frr.pm
+++ b/src/PVE/Network/SDN/Frr.pm
@@ -208,7 +208,7 @@ sub raw_config_to_string {
     my $nodename = PVE::INotify::nodename();
 
     my @final_config = (
-	"frr version 8.5.2",
+	"frr version 10.2.1",
 	"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 473a47080f6e..8ce94871f6b3 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.1
 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 9d8ec609b307..dc485a8b308d 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.1
 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 8dfb6de774fe..a21510cd4238 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.1
 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 82eef1158982..10992c0393b0 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.1
 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 99e933a8c4dc..acf0adcc3717 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.1
 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 2bc55727fb58..e71bc26c5bc8 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.1
 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 28c91a550f2e..a664878944eb 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.1
 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 99e933a8c4dc..acf0adcc3717 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.1
 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 fc8ae67b2a35..b80e9f542ce9 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.1
 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 9d8ec609b307..dc485a8b308d 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.1
 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 9d8ec609b307..dc485a8b308d 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.1
 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 9d8ec609b307..dc485a8b308d 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.1
 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 9d8ec609b307..dc485a8b308d 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.1
 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 fffd4157a8bf..44a580595226 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.1
 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 9ec8c018f97f..0eca3c4124f7 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.1
 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 5a7f5c9fd5e5..2cf055d5d153 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.1
 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 5c9bf1adfbae..34c6738c644d 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.1
 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 a87cdc44d54e..00d28d183249 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.1
 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 37f663a32572..8055268f8285 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.1
 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 264a7c422c11..a1d06725cdd9 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.1
 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 4e8e0c12deda..15d057cb93b8 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.1
 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 5bdb148f94a4..65b70f8be6ad 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.1
 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 9d8ec609b307..dc485a8b308d 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.1
 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 pve-manager v2 01/11] api: use new generalized frr and etc network config helper functions
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (43 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 19/19] frr: bump frr config version to 10.2.1 Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-manager v2 02/11] fabric: add common interface panel Gabriel Goller
                   ` (12 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

pve-network added new generalized frr generation and
etc/network/interfaces generations methods. Starting with this series,
not only the zones plugin edits the /etc/network/interfaces.d/sdn file, but
the fabrics as well. The new fabrics are also implemented in rust, so
they are not a normal ControllerPlugin – that's why the frr generation
has also been factored out.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 PVE/API2/Network.pm | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/PVE/API2/Network.pm b/PVE/API2/Network.pm
index 12ee6cca057f..f5b90baeeade 100644
--- a/PVE/API2/Network.pm
+++ b/PVE/API2/Network.pm
@@ -798,7 +798,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();
 	    }
 
@@ -811,7 +811,7 @@ __PACKAGE__->register_method({
 	    PVE::Tools::run_command(['ifreload', '-a'], errfunc => $err);
 
 	    if ($have_sdn) {
-		PVE::Network::SDN::generate_controller_config(1);
+		PVE::Network::SDN::generate_frr_config(1);
 	    }
 	};
 	return $rpcenv->fork_worker('srvreload', 'networking', $authuser, $worker);
@@ -868,3 +868,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 v2 02/11] fabric: add common interface panel
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (44 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-manager v2 01/11] api: use new generalized frr and etc network config helper functions Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-manager v2 03/11] fabric: add OpenFabric interface properties Gabriel Goller
                   ` (11 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 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.

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

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 www/manager6/Makefile              |   1 +
 www/manager6/sdn/fabrics/Common.js | 292 +++++++++++++++++++++++++++++
 2 files changed, 293 insertions(+)
 create mode 100644 www/manager6/sdn/fabrics/Common.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index c94a5cdfbf70..7df96f58eb1f 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -303,6 +303,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 000000000000..434feb0582b1
--- /dev/null
+++ b/www/manager6/sdn/fabrics/Common.js
@@ -0,0 +1,292 @@
+Ext.define('PVE.sdn.Fabric.InterfacePanel', {
+    extend: 'Ext.grid.Panel',
+    xtype: 'pveSDNFabricsInterfacePanel',
+    mixins: ['Ext.form.field.Field'],
+
+    networkInterfaces: undefined,
+    parentClass: undefined,
+
+    selectionChange: function(_grid, _selection) {
+	let me = this;
+	me.value = me.getSelection().map((rec) => {
+	    let submitValue = structuredClone(rec.data);
+	    delete submitValue.selected;
+	    delete submitValue.isDisabled;
+	    delete submitValue.statusIcon;
+	    delete submitValue.statusTooltip;
+	    delete submitValue.type;
+	    return PVE.Parser.printPropertyString(submitValue);
+	});
+	me.checkChange();
+    },
+
+    getValue: function() {
+        let me = this;
+        return me.value ?? [];
+    },
+
+    setValue: function(value) {
+        let me = this;
+
+        value ??= [];
+
+        me.updateSelectedInterfaces(value);
+
+        return me.mixins.field.setValue.call(me, value);
+    },
+
+    addInterfaces: function(fabricInterfaces) {
+	let me = this;
+	if (me.networkInterfaces) {
+	    let nodeInterfaces = me.networkInterfaces
+		.map((elem) => {
+		    const obj = {
+			name: elem.iface,
+			type: elem.type,
+			ip: elem.cidr,
+			ipv6: elem.cidr6,
+		    };
+		    return obj;
+		});
+
+	    if (fabricInterfaces) {
+		// Map existing node interfaces with fabric data
+		nodeInterfaces = nodeInterfaces.map(i => {
+		    let matchingInterface = fabricInterfaces.find(j => j.name === i.name);
+		    if (matchingInterface) {
+			if ((matchingInterface.ip && i.ip) || (matchingInterface.ipv6 && i.ipv6)) {
+			    i.statusIcon = 'warning fa-warning';
+			    i.statusTooltip = gettext('Interface already has an address configured in /etc/network/interfaces');
+			} else if (i.ip || i.ipv6) {
+			    i.statusIcon = 'warning fa-warning';
+			    i.statusTooltip = gettext('Configure the ip-address using the fabrics interface');
+			    i.isDisabled = true;
+			}
+		    }
+		    return Object.assign(i, matchingInterface);
+		});
+
+		// Add any fabric interface that doesn't exist in node_interfaces
+		for (const fabricInterface of fabricInterfaces) {
+		    if (!nodeInterfaces.some(nodeInterface => nodeInterface.name === fabricInterface.name)) {
+			nodeInterfaces.push({
+			    name: fabricInterface.name,
+			    statusIcon: 'warning fa-warning',
+			    statusTooltip: gettext('Interface not found on node'),
+			    ...fabricInterface,
+			});
+		    }
+		}
+		let store = me.getStore();
+		store.setData(nodeInterfaces);
+	    } else {
+		let store = me.getStore();
+		store.setData(nodeInterfaces);
+	    }
+	} else if (fabricInterfaces) {
+	    // We could not get the available interfaces of the node, so we display the configured ones only.
+	    const interfaces = fabricInterfaces.map((elem) => {
+		const obj = {
+		    name: elem.name,
+		    ...elem,
+		};
+		return obj;
+	    });
+
+	    let store = me.getStore();
+	    store.setData(interfaces);
+	} else {
+	    console.warn("no fabric_interfaces and cluster_interfaces available!");
+	}
+    },
+
+    updateSelectedInterfaces: function(values) {
+	let me = this;
+	if (values) {
+	    let recs = [];
+	    let store = me.getStore();
+
+	    for (const i of values) {
+		let rec = store.getById(i.name);
+		if (rec) {
+		    recs.push(rec);
+		}
+	    }
+	    me.suspendEvent('change');
+	    me.setSelection();
+	    me.setSelection(recs);
+	} else {
+	    me.suspendEvent('change');
+	    me.setSelection();
+	}
+	me.resumeEvent('change');
+    },
+
+    setNetworkInterfaces: function(networkInterfaces) {
+	this.networkInterfaces = networkInterfaces;
+    },
+
+    getSubmitData: function() {
+	let records = this.getSelection().map((record) => {
+	    let submitData = structuredClone(record.data);
+	    delete submitData.selected;
+	    delete submitData.isDisabled;
+	    delete submitData.statusIcon;
+	    delete submitData.statusTooltip;
+	    delete submitData.type;
+
+	    // Delete any properties that are null or undefined
+	    Object.keys(submitData).forEach(function(key) {
+		if (submitData[key] === null || submitData[key] === undefined || submitData[key] === '') {
+		    delete submitData[key];
+		}
+	    });
+
+	    return Proxmox.Utils.printPropertyString(submitData);
+	});
+
+	// Only include interfaces in the submission if there are selected interfaces
+	if (records.length === 0) {
+	    return {};
+	}
+
+	return {
+	    'interfaces': records,
+	};
+    },
+
+    controller: {
+	onValueChange: function(field, value) {
+	    let me = this;
+	    let record = field.getWidgetRecord();
+	    let column = field.getWidgetColumn();
+	    if (record) {
+	        record.set(column.dataIndex, value);
+	        record.commit();
+
+	        me.getView().checkChange();
+	        me.getView().selectionChange();
+	    }
+	},
+
+	control: {
+	    'field': {
+		change: 'onValueChange',
+	    },
+	},
+    },
+
+    selModel: {
+	type: 'checkboxmodel',
+	mode: 'SIMPLE',
+    },
+
+    listeners: {
+	selectionchange: function() {
+	    this.selectionChange(...arguments);
+	},
+    },
+
+    commonColumns: [
+	{
+	    text: gettext('Status'),
+	    dataIndex: 'status',
+	    width: 30,
+	    renderer: function(value, metaData, record) {
+		const icon = record.data.statusIcon || '';
+		const tooltip = record.data.statusTooltip || '';
+
+		if (tooltip) {
+		    metaData.tdAttr = `data-qtip="${Ext.htmlEncode(Ext.htmlEncode(tooltip))}"`;
+		}
+
+		if (icon) {
+		    return `<i class="fa ${icon}"></i>`;
+		}
+
+		return value || '';
+	    },
+
+	},
+	{
+	    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: [],
+
+    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();
+    },
+});
+
+
+Ext.define('Pve.sdn.Fabric', {
+    extend: 'Ext.data.Model',
+    idProperty: 'name',
+    fields: [
+	'name',
+	'type',
+    ],
+});
+
+Ext.define('Pve.sdn.Node', {
+    extend: 'Ext.data.Model',
+    idProperty: 'name',
+    fields: [
+	'name',
+	'fabric',
+	'type',
+    ],
+});
+
+Ext.define('Pve.sdn.Interface', {
+    extend: 'Ext.data.Model',
+    idProperty: 'name',
+    fields: [
+	'name',
+	'ip',
+	'ipv6',
+	'passive',
+	'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 v2 03/11] fabric: add OpenFabric interface properties
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (45 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-manager v2 02/11] fabric: add common interface panel Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-manager v2 04/11] fabric: add OSPF " Gabriel Goller
                   ` (10 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

This component extends the common Fabric InterfacePanel and overwrites
adds some items to it. These are all the available items when
configuring an openfabric interface. Most of these are hidden so as to
not clutter the interface too much with unnecessary elements, these are
used mainly for setting interface or node-specific properties, otherwise
the global fabric-specific properties should be used.

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  | 64 +++++++++++++++++++
 2 files changed, 65 insertions(+)
 create mode 100644 www/manager6/sdn/fabrics/openfabric/InterfacePanel.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 7df96f58eb1f..afad1b7f4d87 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -304,6 +304,7 @@ JSSRC= 							\
 	sdn/zones/VlanEdit.js				\
 	sdn/zones/VxlanEdit.js				\
 	sdn/fabrics/Common.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 000000000000..6d6e0797becc
--- /dev/null
+++ b/www/manager6/sdn/fabrics/openfabric/InterfacePanel.js
@@ -0,0 +1,64 @@
+Ext.define('PVE.sdn.Fabric.OpenFabric.InterfacePanel', {
+    extend: 'PVE.sdn.Fabric.InterfacePanel',
+
+    additionalColumns: [
+	{
+	    text: gettext('IPv6'),
+	    xtype: 'widgetcolumn',
+	    dataIndex: 'ipv6',
+	    flex: 1,
+	    widget: {
+		xtype: 'proxmoxtextfield',
+		isFormField: false,
+		bind: {
+		    disabled: '{record.isDisabled}',
+		},
+	    },
+	},
+	{
+	    text: gettext('Passive'),
+	    xtype: 'widgetcolumn',
+	    dataIndex: 'passive',
+	    flex: 2,
+	    hidden: true,
+	    widget: {
+		xtype: 'checkbox',
+		isFormField: false,
+	    },
+	},
+	{
+	    text: gettext('Hello Interval'),
+	    xtype: 'widgetcolumn',
+	    dataIndex: 'hello_interval',
+	    flex: 2,
+	    hidden: true,
+	    widget: {
+		xtype: 'proxmoxintegerfield',
+		isFormField: false,
+	    },
+	},
+	{
+	    text: gettext('Hello Multiplier'),
+	    xtype: 'widgetcolumn',
+	    dataIndex: 'hello_multiplier',
+	    flex: 2,
+	    hidden: true,
+	    widget: {
+		xtype: 'proxmoxintegerfield',
+		isFormField: false,
+	    },
+	},
+	{
+	    text: gettext('CSNP Interval'),
+	    xtype: 'widgetcolumn',
+	    dataIndex: 'csnp_interval',
+	    flex: 2,
+	    hidden: true,
+	    widget: {
+		xtype: 'proxmoxintegerfield',
+		isFormField: 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 v2 04/11] fabric: add OSPF interface properties
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (46 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-manager v2 03/11] fabric: add OpenFabric interface properties Gabriel Goller
@ 2025-04-04 16:28 ` Gabriel Goller
  2025-04-04 16:29 ` [pve-devel] [PATCH pve-manager v2 05/11] fabric: add generic node edit panel Gabriel Goller
                   ` (9 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:28 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

Extends PVE.sdn.Fabric.InterfacePanel to add OSPF-specific interface
properties including passive mode and unnumbered network-type. Passive
is hidden by default to reduce UI complexity, as it's rarely used.

Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 www/manager6/Makefile                         |  1 +
 .../sdn/fabrics/ospf/InterfacePanel.js        | 27 +++++++++++++++++++
 2 files changed, 28 insertions(+)
 create mode 100644 www/manager6/sdn/fabrics/ospf/InterfacePanel.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index afad1b7f4d87..980c992432de 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -305,6 +305,7 @@ JSSRC= 							\
 	sdn/zones/VxlanEdit.js				\
 	sdn/fabrics/Common.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 000000000000..af4577c21e38
--- /dev/null
+++ b/www/manager6/sdn/fabrics/ospf/InterfacePanel.js
@@ -0,0 +1,27 @@
+Ext.define('PVE.sdn.Fabric.Ospf.InterfacePanel', {
+    extend: 'PVE.sdn.Fabric.InterfacePanel',
+
+    additionalColumns: [
+	{
+	    text: gettext('Passive'),
+	    xtype: 'widgetcolumn',
+	    dataIndex: 'passive',
+	    flex: 1,
+	    hidden: true,
+	    widget: {
+		xtype: 'checkbox',
+		isFormField: false,
+	    },
+	},
+	{
+	    text: gettext('Unnumbered'),
+	    xtype: 'widgetcolumn',
+	    dataIndex: 'unnumbered',
+	    flex: 1,
+	    widget: {
+		xtype: 'checkbox',
+		isFormField: 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 v2 05/11] fabric: add generic node edit panel
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (47 preceding siblings ...)
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-manager v2 04/11] fabric: add OSPF " Gabriel Goller
@ 2025-04-04 16:29 ` Gabriel Goller
  2025-04-04 16:29 ` [pve-devel] [PATCH pve-manager v2 06/11] fabric: add generic fabric " Gabriel Goller
                   ` (8 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:29 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

This component is generic over both protocols and is used for edit and
creation of nodes. It shows a node-selector, the router-id and a list of
interfaces available to be selected. If the node is not reachable, we
show a read-only panel with 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 980c992432de..a0a739d7de99 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -304,6 +304,7 @@ JSSRC= 							\
 	sdn/zones/VlanEdit.js				\
 	sdn/zones/VxlanEdit.js				\
 	sdn/fabrics/Common.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 000000000000..adace6a7bb28
--- /dev/null
+++ b/www/manager6/sdn/fabrics/NodeEdit.js
@@ -0,0 +1,224 @@
+Ext.define('PVE.sdn.Fabric.Node.InputPanel', {
+    extend: 'Proxmox.panel.InputPanel',
+
+    viewModel: {},
+
+    isCreate: undefined,
+    protocol: undefined,
+    loadClusterInterfaces: undefined,
+    interfaceSelector: undefined,
+    nodeNotAccessibleWarning: undefined,
+    alreadyConfiguredNodes: undefined,
+
+    onSetValues: function(values) {
+	let me = this;
+	me.interfaceSelector.setNetworkInterfaces(values.networkInterfaces);
+	if (values.node) {
+	    // this means we are in edit mode and we have a config
+	    me.interfaceSelector.addInterfaces(values.node.interfaces);
+	    me.interfaceSelector.updateSelectedInterfaces(values.node.interfaces);
+	    me.interfaceSelector.originalValue = values.node.interfaces;
+	    return {
+		node_id: values.node.node_id,
+		router_id: values.node.router_id,
+		interfaces: values.node.interfaces,
+		digest: values.node.digest,
+	    };
+	} else if (values.router_id) {
+	    // if only a single router_id is set, we only want to update
+	    // that parameter, without reloading the interface panel.
+	    return { router_id: values.router_id };
+	} else {
+	    // this means we are in create mode, so don't select any interfaces
+	    me.interfaceSelector.addInterfaces(null);
+	    me.interfaceSelector.updateSelectedInterfaces(null);
+	    return {};
+	}
+    },
+
+    initComponent: function() {
+	let me = this;
+	const PROTOCOL_INTERFACE_PANELS = {
+	    'openfabric': 'PVE.sdn.Fabric.OpenFabric.InterfacePanel',
+	    'ospf': 'PVE.sdn.Fabric.Ospf.InterfacePanel',
+	};
+
+	me.interfaceSelector = Ext.create(PROTOCOL_INTERFACE_PANELS[me.protocol], {
+	    name: 'interfaces',
+	    parentClass: me.isCreate ? me : undefined,
+	});
+	me.items = [
+	    {
+		xtype: 'pveNodeSelector',
+		reference: 'nodeselector',
+		fieldLabel: gettext('Node'),
+		labelWidth: 120,
+		name: 'node_id',
+		allowBlank: false,
+		disabled: !me.isCreate,
+		disallowedNodes: me.alreadyConfiguredNodes,
+		onlineValidator: me.isCreate,
+		autoSelect: me.isCreate,
+		listeners: {
+		    change: function(f, value) {
+			if (me.isCreate) {
+			    me.loadClusterInterfaces(value, (result) => {
+				me.setValues({ networkInterfaces: result });
+			    });
+			}
+		    },
+		},
+		listConfig: {
+		    columns: [
+			{
+			    header: gettext('Node'),
+			    dataIndex: 'node',
+			    sortable: true,
+			    hideable: false,
+			    flex: 1,
+			},
+		    ],
+		},
+
+	    },
+	    me.nodeNotAccessibleWarning,
+	    {
+		xtype: 'textfield',
+		fieldLabel: gettext('Loopback IP'),
+		labelWidth: 120,
+		name: 'router_id',
+		allowBlank: false,
+	    },
+	    me.interfaceSelector,
+	];
+
+	if (!me.isCreate) {
+	    me.items.push({
+		xtype: 'textfield',
+		name: 'digest',
+		hidden: true,
+		allowBlank: false,
+	    });
+	}
+
+	me.callParent();
+    },
+});
+
+Ext.define('PVE.sdn.Fabric.Node.Edit', {
+    extend: 'Proxmox.window.Edit',
+    xtype: 'pveSDNFabricAddNode',
+
+    width: 800,
+    subject: gettext('Node'),
+
+    protocol: undefined,
+    isCreate: undefined,
+
+    controller: {
+	xclass: 'Ext.app.ViewController',
+    },
+
+
+    submitUrl: function(url, values) {
+	let me = this;
+	return `${me.url}/${me.extraRequestParams.fabric}/node`;
+    },
+
+    loadClusterInterfaces: function(node, onSuccess) {
+	Proxmox.Utils.API2Request({
+	      url: `/api2/extjs/nodes/${node}/network`,
+	      method: 'GET',
+	      success: function(response, _opts) {
+		  onSuccess(response.result.data);
+	      },
+	      // No failure callback because this api call can't fail, it
+	      // just hangs the request :) (if the node doesn't exist it gets proxied)
+	});
+    },
+    loadFabricInterfaces: function(protocol, fabric, node, onSuccess, onFailure) {
+	Proxmox.Utils.API2Request({
+	      url: `/cluster/sdn/fabrics/${protocol}/${fabric}/node/${node}`,
+	      method: 'GET',
+	      success: function(response, _opts) {
+		  onSuccess(response.result.data);
+	      },
+	      failure: onFailure,
+	});
+    },
+    loadAllAvailableNodes: function(onSuccess) {
+	Proxmox.Utils.API2Request({
+	      url: `/cluster/config/nodes`,
+	      method: 'GET',
+	      success: function(response, _opts) {
+		  onSuccess(response.result.data);
+	      },
+	});
+    },
+
+    initComponent: function() {
+	let me = this;
+	me.onlineHelp = `pvesdn_${me.protocol}_fabric`;
+	me.url = '/cluster/sdn/fabrics/' + me.protocol;
+
+	me.nodeNotAccessibleWarning = Ext.create('Proxmox.form.field.DisplayEdit', {
+	    userCls: 'pmx-hint',
+	    value: gettext('The node is not accessible.'),
+	    hidden: true,
+	});
+
+	let ipanel = Ext.create('PVE.sdn.Fabric.Node.InputPanel', {
+	    nodeNotAccessibleWarning: me.nodeNotAccessibleWarning,
+	    isCreate: me.isCreate,
+	    protocol: me.protocol,
+	    loadClusterInterfaces: me.loadClusterInterfaces,
+	    alreadyConfiguredNodes: me.alreadyConfiguredNodes,
+	});
+
+	Ext.apply(me, {
+	    items: [ipanel],
+	});
+
+	me.callParent();
+
+	if (!me.isCreate) {
+	    me.loadAllAvailableNodes((allNodes) => {
+		if (allNodes.some(i => i.name === me.node)) {
+		    me.loadClusterInterfaces(me.node, (clusterResult) => {
+			me.loadFabricInterfaces(me.protocol, me.fabric, me.node, (fabricResult) => {
+			    fabricResult.interfaces = fabricResult.interfaces
+				.map(i => PVE.Parser.parsePropertyString(i));
+
+			    let data = {
+				node: fabricResult,
+				networkInterfaces: clusterResult,
+			    };
+
+			    ipanel.setValues(data);
+			});
+		    });
+		} else {
+		    me.nodeNotAccessibleWarning.setHidden(false);
+		    // If the node is not currently in the cluster and not available (we can't get it's interfaces).
+		    me.loadFabricInterfaces(me.protocol, me.fabric, me.node, (fabricResult) => {
+			fabricResult.interfaces = fabricResult.interfaces
+			    .map(i => PVE.Parser.parsePropertyString(i));
+
+			let data = {
+			    node: fabricResult,
+			};
+
+			ipanel.setValues(data);
+		    });
+		}
+	    });
+	}
+
+	if (me.isCreate) {
+	    me.method = 'POST';
+	} else {
+	    me.method = 'PUT';
+	}
+    },
+});
+
-- 
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 v2 06/11] fabric: add generic fabric edit panel
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (48 preceding siblings ...)
  2025-04-04 16:29 ` [pve-devel] [PATCH pve-manager v2 05/11] fabric: add generic node edit panel Gabriel Goller
@ 2025-04-04 16:29 ` Gabriel Goller
  2025-04-04 16:29 ` [pve-devel] [PATCH pve-manager v2 07/11] fabric: add OpenFabric " Gabriel Goller
                   ` (7 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:29 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

Add generic component to add and edit Fabrics. 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 | 44 ++++++++++++++++++++++++++
 2 files changed, 45 insertions(+)
 create mode 100644 www/manager6/sdn/fabrics/FabricEdit.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index a0a739d7de99..41bd0830f816 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -305,6 +305,7 @@ JSSRC= 							\
 	sdn/zones/VxlanEdit.js				\
 	sdn/fabrics/Common.js				\
 	sdn/fabrics/NodeEdit.js				\
+	sdn/fabrics/FabricEdit.js				\
 	sdn/fabrics/openfabric/InterfacePanel.js				\
 	sdn/fabrics/ospf/InterfacePanel.js	\
 	storage/ContentView.js				\
diff --git a/www/manager6/sdn/fabrics/FabricEdit.js b/www/manager6/sdn/fabrics/FabricEdit.js
new file mode 100644
index 000000000000..f3e966cb6e62
--- /dev/null
+++ b/www/manager6/sdn/fabrics/FabricEdit.js
@@ -0,0 +1,44 @@
+Ext.define('PVE.sdn.Fabric.Fabric.Edit', {
+    extend: 'Proxmox.window.Edit',
+
+    isCreate: undefined,
+
+    viewModel: {
+	data: {
+	    isCreate: true,
+	},
+    },
+
+    submitUrl: function(url, values) {
+	let me = this;
+	return `${me.url}`;
+    },
+
+    initComponent: function() {
+	let me = this;
+
+	let view = me.getViewModel();
+	view.set('isCreate', me.isCreate);
+
+	me.method = me.isCreate ? 'POST' : 'PUT';
+
+	if (!me.isCreate) {
+	    me.items.push({
+		xtype: 'textfield',
+		name: 'digest',
+		hidden: true,
+		allowBlank: false,
+	    });
+	}
+
+	me.callParent();
+
+	if (!me.isCreate) {
+	    me.load({
+		success: function(response, opts) {
+		    me.setValues(response.result.data);
+		},
+	    });
+	}
+    },
+});
-- 
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 v2 07/11] fabric: add OpenFabric fabric edit panel
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (49 preceding siblings ...)
  2025-04-04 16:29 ` [pve-devel] [PATCH pve-manager v2 06/11] fabric: add generic fabric " Gabriel Goller
@ 2025-04-04 16:29 ` Gabriel Goller
  2025-04-04 16:29 ` [pve-devel] [PATCH pve-manager v2 08/11] fabric: add OSPF " Gabriel Goller
                   ` (6 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:29 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

Component that extends the common FabricEdit component and adds the
OpenFabric-specific items to it. The only editable property is the
global hello-interval. The others could be made modifiable in future,
but need to be checked closely (e.g., for loopback_prefix we need to
check the loopback_ip of every node, and for the Name we would need to
modify the config of every node).

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      | 37 +++++++++++++++++++
 2 files changed, 38 insertions(+)
 create mode 100644 www/manager6/sdn/fabrics/openfabric/FabricEdit.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 41bd0830f816..f7cba245f164 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -307,6 +307,7 @@ JSSRC= 							\
 	sdn/fabrics/NodeEdit.js				\
 	sdn/fabrics/FabricEdit.js				\
 	sdn/fabrics/openfabric/InterfacePanel.js				\
+	sdn/fabrics/openfabric/FabricEdit.js				\
 	sdn/fabrics/ospf/InterfacePanel.js	\
 	storage/ContentView.js				\
 	storage/BackupView.js				\
diff --git a/www/manager6/sdn/fabrics/openfabric/FabricEdit.js b/www/manager6/sdn/fabrics/openfabric/FabricEdit.js
new file mode 100644
index 000000000000..b080f5dcf9d7
--- /dev/null
+++ b/www/manager6/sdn/fabrics/openfabric/FabricEdit.js
@@ -0,0 +1,37 @@
+Ext.define('PVE.sdn.Fabric.OpenFabric.Fabric.Edit', {
+    extend: 'PVE.sdn.Fabric.Fabric.Edit',
+    xtype: 'pveSDNOpenFabricRouteEdit',
+
+    subject: 'OpenFabric',
+    onlineHelp: 'pvesdn_openfabric_fabric',
+    url: '/cluster/sdn/fabrics/openfabric',
+
+    items: [
+	{
+	    xtype: 'textfield',
+	    fieldLabel: gettext('Name'),
+	    labelWidth: 120,
+	    name: 'fabric_id',
+	    bind: {
+		disabled: '{!isCreate}',
+	    },
+	},
+	{
+	    xtype: 'textfield',
+	    fieldLabel: gettext('Loopback IP Prefix'),
+	    labelWidth: 120,
+	    name: 'loopback_prefix',
+	    allowBlank: false,
+	    bind: {
+		disabled: '{!isCreate}',
+	    },
+	},
+	{
+	    xtype: 'proxmoxintegerfield',
+	    fieldLabel: gettext('Hello Interval'),
+	    labelWidth: 120,
+	    name: 'hello_interval',
+	    allowBlank: 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 v2 08/11] fabric: add OSPF fabric edit panel
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (50 preceding siblings ...)
  2025-04-04 16:29 ` [pve-devel] [PATCH pve-manager v2 07/11] fabric: add OpenFabric " Gabriel Goller
@ 2025-04-04 16:29 ` Gabriel Goller
  2025-04-04 16:29 ` [pve-devel] [PATCH pve-manager v2 09/11] fabrics: Add main FabricView Gabriel Goller
                   ` (5 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:29 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

Extends the common FabricEdit component and adds the OSPF-specific items
to it. The only modifiable item is the area. The other properties could
be made modifiable in the future, but would need to be checked closely
(e.g., to change loopback_prefix, we would need to check every node, and
to change the name we would need to modify ever node's config as well.)

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 | 40 +++++++++++++++++++++
 2 files changed, 41 insertions(+)
 create mode 100644 www/manager6/sdn/fabrics/ospf/FabricEdit.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index f7cba245f164..39abd8292044 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -309,6 +309,7 @@ JSSRC= 							\
 	sdn/fabrics/openfabric/InterfacePanel.js				\
 	sdn/fabrics/openfabric/FabricEdit.js				\
 	sdn/fabrics/ospf/InterfacePanel.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 000000000000..34f52ce31376
--- /dev/null
+++ b/www/manager6/sdn/fabrics/ospf/FabricEdit.js
@@ -0,0 +1,40 @@
+Ext.define('PVE.sdn.Fabric.Ospf.Fabric.Edit', {
+    extend: 'PVE.sdn.Fabric.Fabric.Edit',
+    xtype: 'pveSDNOspfRouteEdit',
+
+    subject: 'OSPF',
+
+    onlineHelp: 'pvesdn_ospf_fabric',
+
+    url: '/cluster/sdn/fabrics/ospf',
+
+    items: [
+	{
+	    xtype: 'textfield',
+	    fieldLabel: gettext('Name'),
+	    labelWidth: 120,
+	    name: 'fabric_id',
+	    allowBlank: false,
+	    bind: {
+		disabled: '{!isCreate}',
+	    },
+	},
+	{
+	    xtype: 'textfield',
+	    fieldLabel: gettext('Area'),
+	    labelWidth: 120,
+	    name: 'area',
+	    allowBlank: false,
+	},
+	{
+	    xtype: 'textfield',
+	    fieldLabel: gettext('Loopback IP Prefix'),
+	    labelWidth: 120,
+	    name: 'loopback_prefix',
+	    allowBlank: false,
+	    bind: {
+		disabled: '{!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 v2 09/11] fabrics: Add main FabricView
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (51 preceding siblings ...)
  2025-04-04 16:29 ` [pve-devel] [PATCH pve-manager v2 08/11] fabric: add OSPF " Gabriel Goller
@ 2025-04-04 16:29 ` Gabriel Goller
  2025-04-04 16:29 ` [pve-devel] [PATCH pve-manager v2 10/11] utils: avoid line-break in pending changes message Gabriel Goller
                   ` (4 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:29 UTC (permalink / raw)
  To: pve-devel

TreeView that shows all the fabrics and nodes in a hierarchical
structure. It also shows all the pending changes from the
running-config.

We decided against including all the interfaces (as children of nodes)
because otherwise the indentation would be too much. So to keep it
simple, we removed the interface entries and also moved the protocol to
the column (instead of having two root nodes for each protocol).

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Co-authored-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 www/manager6/Makefile           |   1 +
 www/manager6/dc/Config.js       |   8 +
 www/manager6/sdn/FabricsView.js | 419 ++++++++++++++++++++++++++++++++
 3 files changed, 428 insertions(+)
 create mode 100644 www/manager6/sdn/FabricsView.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 39abd8292044..a959860fe73a 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -303,6 +303,7 @@ JSSRC= 							\
 	sdn/zones/SimpleEdit.js				\
 	sdn/zones/VlanEdit.js				\
 	sdn/zones/VxlanEdit.js				\
+	sdn/FabricsView.js				\
 	sdn/fabrics/Common.js				\
 	sdn/fabrics/NodeEdit.js				\
 	sdn/fabrics/FabricEdit.js				\
diff --git a/www/manager6/dc/Config.js b/www/manager6/dc/Config.js
index 74728c8320e9..68f7be8d6042 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 000000000000..f0faa709264e
--- /dev/null
+++ b/www/manager6/sdn/FabricsView.js
@@ -0,0 +1,419 @@
+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',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    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 rec.data.fabric_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') {
+		    return "";
+		}
+
+		const PROTOCOL_DISPLAY_NAMES = {
+		    'openfabric': 'OpenFabric',
+		    'ospf': 'OSPF',
+		};
+
+		return PVE.Utils.render_sdn_pending(rec, PROTOCOL_DISPLAY_NAMES[value], 'protocol');
+	    },
+	},
+	{
+	    text: gettext('Loopback IP'),
+	    dataIndex: 'router_id',
+	    width: 150,
+	    renderer: function(value, metaData, rec) {
+		if (rec.data.type === 'fabric') {
+		    return PVE.Utils.render_sdn_pending(rec, rec.data.loopback_prefix, 'loopback_prefix');
+		}
+
+		return PVE.Utils.render_sdn_pending(rec, value, 'router_id');
+	    },
+	},
+	{
+	    header: gettext('Interfaces'),
+	    width: 100,
+	    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();
+		return Ext.htmlEncode(names.join(", "));
+	    },
+	},
+	{
+	    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: ['name'],
+	model: 'PVE.sdn.Fabric.TreeModel',
+    },
+
+    layout: 'fit',
+    rootVisible: false,
+    animate: false,
+
+    initComponent: function() {
+	let me = this;
+
+	let addButton = new Proxmox.button.Button({
+	    text: gettext('Add Node'),
+	    handler: 'addActionTbar',
+	    disabled: true,
+	});
+
+	let setAddButtonStatus = function() {
+	    let selection = me.view.getSelection();
+
+	    if (selection.length === 0) {
+		return;
+	    }
+
+	    addButton.setDisabled(selection[0].data.type !== 'fabric');
+	};
+
+	Ext.apply(me, {
+	    tbar: [
+		{
+		    text: gettext('Add Fabric'),
+		    menu: [
+			{
+			    text: 'OpenFabric',
+			    handler: 'openAddOpenFabricWindow',
+			},
+			{
+			    text: 'OSPF',
+			    handler: 'openAddOspfWindow',
+			},
+		    ],
+		},
+		addButton,
+		{
+		    xtype: 'proxmoxButton',
+		    text: gettext('Reload'),
+		    handler: 'reload',
+		},
+	    ],
+	    listeners: {
+		selectionchange: setAddButtonStatus,
+	    },
+	});
+
+	me.callParent();
+    },
+
+    controller: {
+	xclass: 'Ext.app.ViewController',
+
+	mapTree: function(allFabrics) {
+	    return allFabrics.filter(e => e.type === "fabric").map((fabric) => {
+		if (!fabric.state || fabric.state !== 'deleted') {
+		    fabric.children = allFabrics.filter(e => e.type === "node")
+			.filter((node) =>
+			    node.fabric_id === fabric.fabric_id && node.protocol === fabric.protocol)
+				.map((node) => {
+				    Object.assign(node, {
+					leaf: true,
+					iconCls: 'fa fa-desktop x-fa-treepanel',
+					tree_id: `${fabric.protocol}_${node.id}`,
+				    });
+
+				    return node;
+				});
+		}
+
+		Object.assign(fabric, {
+		    expanded: true,
+		    iconCls: 'fa fa-road x-fa-treepanel',
+		    tree_id: `${fabric.protocol}_${fabric.fabric_id}`,
+		});
+
+		return fabric;
+	    });
+	},
+
+	formatResult: function(entity, protocol) {
+	    if (entity.state && entity.state === "new") {
+		Object.assign(entity, entity.pending);
+	    }
+
+	    entity.protocol = protocol;
+
+	    return entity;
+	},
+
+	reload: function() {
+	    let me = this;
+
+	    Proxmox.Utils.API2Request({
+		url: `/cluster/sdn/fabrics/all?pending=1`,
+		method: 'GET',
+		success: function(response, opts) {
+		    let allFabrics = [];
+
+		    if (response.result.data.ospf) {
+			let data = response.result.data.ospf.map((entry) => me.formatResult(entry, "ospf"));
+			allFabrics = allFabrics.concat(data);
+		    }
+
+		    if (response.result.data.openfabric) {
+			let data = response.result.data.openfabric.map((entry) => me.formatResult(entry, "openfabric"));
+			allFabrics = allFabrics.concat(data);
+		    }
+
+		    me.getView().setRootNode({
+			name: '__root',
+			expanded: true,
+			children: me.mapTree(allFabrics),
+		    });
+		},
+	    });
+	},
+
+	getFabricEditPanel: function(type) {
+	    const FABRIC_PANELS = {
+		'openfabric': 'PVE.sdn.Fabric.OpenFabric.Fabric.Edit',
+		'ospf': 'PVE.sdn.Fabric.Ospf.Fabric.Edit',
+	    };
+
+	    return FABRIC_PANELS[type];
+	},
+
+	addActionTreeColumn: function(_grid, _rI, _cI, _item, _e, rec) {
+	    this.addAction(rec);
+	},
+
+	addActionTbar: function() {
+	    let me = this;
+
+	    let selection = me.view.getSelection();
+
+	    if (selection.length === 0) {
+		return;
+	    }
+
+	    if (selection[0].data.type === 'fabric') {
+		me.addAction(selection[0]);
+	    }
+	},
+
+	addAction: function(rec) {
+	    let me = this;
+
+	    let component = 'PVE.sdn.Fabric.Node.Edit';
+
+	    let extraRequestParams = {
+		fabric: rec.data.fabric_id,
+	    };
+
+	    let configuredNodes = rec.data.children
+		.filter(node => node.state !== 'deleted')
+		.map(node => node.node_id);
+
+	    let window = Ext.create(component, {
+		autoShow: true,
+		isCreate: true,
+		autoLoad: false,
+		protocol: rec.data.protocol,
+		extraRequestParams,
+		alreadyConfiguredNodes: configuredNodes,
+	    });
+
+	    window.on('destroy', () => me.reload());
+	},
+
+	editAction: function(_grid, _rI, _cI, _item, _e, rec) {
+	    let me = this;
+
+	    let component = '';
+	    let url = '';
+	    let autoLoad = false;
+	    let data = rec.data;
+
+	    if (data.type === 'fabric') {
+		component = me.getFabricEditPanel(data.protocol);
+		url = `/cluster/sdn/fabrics/${data.protocol}/${data.fabric_id}`;
+	    } else if (data.type === 'node') {
+		component = 'PVE.sdn.Fabric.Node.Edit';
+		url = `/cluster/sdn/fabrics/${data.protocol}/${data.fabric_id}/node/${data.node_id}`;
+	    }
+
+	    if (!component) {
+		console.warn(`unknown protocol ${data.protocol} or unknown type ${data.type}`);
+		return;
+	    }
+
+	    let window = Ext.create(component, {
+		autoShow: true,
+		autoLoad: autoLoad,
+		isCreate: false,
+		submitUrl: url,
+		loadUrl: url,
+		fabric: data.fabric_id,
+		protocol: data.protocol,
+		node: data.node_id,
+	    });
+
+	    window.on('destroy', () => me.reload());
+	},
+
+	deleteAction: function(table, rI, cI, item, e, { data }) {
+	    let me = this;
+	    let view = me.getView();
+
+	    let message = '';
+	    if (data.type === "fabric") {
+		message = Ext.String.format(gettext('Are you sure you want to remove the fabric "{0}"?'), data.fabric_id);
+	    } else if (data.type === "node") {
+		message = Ext.String.format(gettext('Are you sure you want to remove the node "{0}" from the fabric "{1}"?'), data.node_id, data.fabric_id);
+	    } else {
+		console.warn("deleteAction: missing type");
+		return;
+	    }
+
+
+	    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;
+		    }
+
+		    let url;
+		    if (data.type === "node") {
+			url = `/cluster/sdn/fabrics/${data.protocol}/${data.fabric_id}/node/${data.node_id}`;
+		    } else if (data.type === "fabric") {
+			url = `/cluster/sdn/fabrics/${data.protocol}/${data.fabric_id}`;
+		    } else {
+			console.warn("deleteAction: missing type");
+		    }
+
+		    Proxmox.Utils.API2Request({
+			url,
+			method: 'DELETE',
+			waitMsgTarget: view,
+			failure: function(response, opts) {
+			    Ext.Msg.alert(Proxmox.Utils.errorText, response.htmlStatus);
+			},
+			callback: () => me.reload(),
+		    });
+		},
+	    });
+	},
+
+	openAddOpenFabricWindow: function() {
+	    let me = this;
+
+	    let window = Ext.create('PVE.sdn.Fabric.OpenFabric.Fabric.Edit', {
+		autoShow: true,
+		autoLoad: false,
+		isCreate: true,
+	    });
+
+	    window.on('destroy', () => me.reload());
+	},
+
+	openAddOspfWindow: function() {
+	    let me = this;
+
+	    let window = Ext.create('PVE.sdn.Fabric.Ospf.Fabric.Edit', {
+		autoShow: true,
+		autoLoad: false,
+		isCreate: true,
+	    });
+
+	    window.on('destroy', () => me.reload());
+	},
+
+	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 v2 10/11] utils: avoid line-break in pending changes message
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (52 preceding siblings ...)
  2025-04-04 16:29 ` [pve-devel] [PATCH pve-manager v2 09/11] fabrics: Add main FabricView Gabriel Goller
@ 2025-04-04 16:29 ` Gabriel Goller
  2025-04-04 16:29 ` [pve-devel] [PATCH pve-manager v2 11/11] ui: permissions: add ACL paths for fabrics Gabriel Goller
                   ` (3 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:29 UTC (permalink / raw)
  To: pve-devel

Remove line-break on sdn "pending configuration" message on removed sdn
objects.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Co-authored-by: Gabriel Goller <g.goller@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@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 14afcf8f067e..95959c683855 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 v2 11/11] ui: permissions: add ACL paths for fabrics
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (53 preceding siblings ...)
  2025-04-04 16:29 ` [pve-devel] [PATCH pve-manager v2 10/11] utils: avoid line-break in pending changes message Gabriel Goller
@ 2025-04-04 16:29 ` Gabriel Goller
  2025-04-04 16:29 ` [pve-devel] [PATCH pve-gui-tests v2 1/1] pve: add sdn/fabrics screenshots Gabriel Goller
                   ` (2 subsequent siblings)
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:29 UTC (permalink / raw)
  To: pve-devel

From: Stefan Hanreich <s.hanreich@proxmox.com>

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 www/manager6/data/PermPathStore.js | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/www/manager6/data/PermPathStore.js b/www/manager6/data/PermPathStore.js
index 72da2e9da4a1..4654d51654c1 100644
--- a/www/manager6/data/PermPathStore.js
+++ b/www/manager6/data/PermPathStore.js
@@ -15,6 +15,8 @@ Ext.define('PVE.data.PermPathStore', {
 	{ 'value': '/mapping/usb' },
 	{ 'value': '/nodes' },
 	{ 'value': '/pool' },
+	{ 'value': '/sdn/fabrics/openfabric' },
+	{ 'value': '/sdn/fabrics/ospf' },
 	{ '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-gui-tests v2 1/1] pve: add sdn/fabrics screenshots
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (54 preceding siblings ...)
  2025-04-04 16:29 ` [pve-devel] [PATCH pve-manager v2 11/11] ui: permissions: add ACL paths for fabrics Gabriel Goller
@ 2025-04-04 16:29 ` Gabriel Goller
  2025-04-04 16:29 ` [pve-devel] [PATCH pve-docs v2 1/1] fabrics: add initial documentation for sdn fabrics Gabriel Goller
  2025-04-07  8:53 ` [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Friedrich Weber
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:29 UTC (permalink / raw)
  To: pve-devel

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 | 197 +++++++++++++++++++++++++++++++++++++
 1 file changed, 197 insertions(+)
 create mode 100755 create_fabrics_screenshots

diff --git a/create_fabrics_screenshots b/create_fabrics_screenshots
new file mode 100755
index 000000000000..89536883f809
--- /dev/null
+++ b/create_fabrics_screenshots
@@ -0,0 +1,197 @@
+#!/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/ospf/test2");
+        $conn->delete("/cluster/sdn/fabrics/openfabric/test1");
+    };
+
+    my $openfabric = {
+        fabric_id => "test1",
+        loopback_prefix => "192.0.2.0/24",
+        hello_interval => "1",
+    };
+    my $ospf = {
+        fabric_id => "test2",
+        area => "0.0.0.0",
+        loopback_prefix => "198.51.100.0/24",
+    };
+    $conn->post("/cluster/sdn/fabrics/openfabric", $openfabric);
+    $conn->post("/cluster/sdn/fabrics/ospf", $ospf);
+}
+
+sub prepare_nodes {
+    my ($conn) = @_;
+    eval {
+        # might not exist, so ignore any errors
+        $conn->delete("/cluster/sdn/fabrics/ospf/test2/node/pve0");
+        $conn->delete("/cluster/sdn/fabrics/ospf/test2/node/pve1");
+        $conn->delete("/cluster/sdn/fabrics/ospf/test2/node/pve2");
+        $conn->delete("/cluster/sdn/fabrics/openfabric/test1/node/pve0");
+        $conn->delete("/cluster/sdn/fabrics/openfabric/test1/node/pve1");
+        $conn->delete("/cluster/sdn/fabrics/openfabric/test1/node/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,
+            router_id => "198.51.100.$last_octet",
+            interfaces => [
+                "name=ens$ospf_iface,unnumbered=true",
+                "name=ens" . ($ospf_iface + 1 . ",unnumbered=true")
+            ],
+        };
+        my $openfabric_node = {
+            node_id => $node_name,
+            router_id => "192.0.2.$last_octet",
+            interfaces => [
+                "name=ens$openfabric_iface",
+                "name=ens" . ($openfabric_iface + 1)
+            ],
+        };
+        
+        $conn->post("/cluster/sdn/fabrics/openfabric/test1/node/", $openfabric_node);
+        $conn->post("/cluster/sdn/fabrics/ospf/test2/node/", $ospf_node);
+    }
+}
+
+sub select_node {
+    my ($self, $node) = @_;
+
+    my $driver = $self->{driver};
+
+    my $js = "let panel = Ext.ComponentQuery.query('pveSDNFabricView')[0];" .
+        "let store = panel.getStore();" .
+        "let record = store.findRecord('fabric_id', '$node');" .
+        "panel.setSelection(record);" .
+        "return true";
+
+    my $res;
+
+    $res = Proxmox::GUITester::verify_scalar_result($driver->execute_script($js));
+    croak "unable to select node '$node'\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, 'fabric_id', 'test1');
+    $gui->setValue($window, 'loopback_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, 'fabric_id', 'test2');
+    $gui->setValue($window, 'area', '0.0.0.0');
+    $gui->setValue($window, 'loopback_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);
+
+    select_node($gui, "test1");
+    sleep_ms(500);
+    
+    $menu = $gui->find_button('Add Node', $panel)->click();
+    $window = $gui->find_dialog("Create: Node");
+    $gui->setValue($window, 'router_id', '192.0.2.1');
+    select_interfaces($gui, "ens19", "ens20");
+    $gui->element_screenshot("gui-datacenter-create-node-openfabric.png", $window);
+    $gui->window_close($window);
+
+    select_node($gui, "test2");
+    sleep_ms(500);
+    $menu = $gui->find_button('Add Node', $panel)->click();
+    $window = $gui->find_dialog("Create: Node");
+    $gui->setValue($window, 'router_id', '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 v2 1/1] fabrics: add initial documentation for sdn fabrics
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (55 preceding siblings ...)
  2025-04-04 16:29 ` [pve-devel] [PATCH pve-gui-tests v2 1/1] pve: add sdn/fabrics screenshots Gabriel Goller
@ 2025-04-04 16:29 ` Gabriel Goller
  2025-04-07  8:53 ` [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Friedrich Weber
  57 siblings, 0 replies; 76+ messages in thread
From: Gabriel Goller @ 2025-04-04 16:29 UTC (permalink / raw)
  To: pve-devel

Add initial documentation for the openfabric and ospf options.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
 pvesdn.adoc | 206 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 206 insertions(+)

diff --git a/pvesdn.adoc b/pvesdn.adoc
index 5d5d27bfbc1f..414f39c5109e 100644
--- a/pvesdn.adoc
+++ b/pvesdn.adoc
@@ -517,6 +517,212 @@ 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
+footnote:[Full Mesh Network for Ceph{webwiki-url}Full_Mesh_Network_for_Ceph_Server]
+(Note that currently you need to add the fabrics prefix to Ceph manually, so:
+`pveceph init --network 192.0.2.0/24`) or as an underlay network for EVPN
+deployments.
+
+Installation
+~~~~~~~~~~~~
+
+The FRR implementations of OpenFabric and OSPF are used, so first the `frr` and
+`frr-pythontools` packages must be installed:
+
+----
+apt update
+apt install frr frr-pythontools
+----
+
+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.
+
+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).
+
+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.
+
+[[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.
+
+Loopback Prefix:: CIDR (IPv4 or IPv6) network range (e.g., 192.0.2.0/24) 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.
+
+[[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.
+
+
+Router-ID:: A unique IPv4 or 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
+(this consistency is automatically managed by {pve}).
+
+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.
+Interface-specific settings override the global fabric settings.
+
+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).
+
+Passive::: When enabled, the interface will not form OpenFabric adjacencies but
+its networks will still be advertised.
+
+Hello Interval::: Controls how frequently (in seconds) hello packets are sent
+on this specific interface. Lower values detect failures faster but increase
+network traffic. 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. The default value is 10 seconds.
+
+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.
+
+Loopback Prefix:: CIDR (only IPv4) 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.
+
+Router-ID:: A unique IPv4 address 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 when enabling
+the additional columns:
+
+IP::: A IPv4 that should get automatically configured on this interface. Must
+include the netmask (e.g. /31)
+
+Passive::: When enabled, the interface will not form OSPF adjacencies but
+its networks will still be advertised.
+
+Unnumbered::: When enabled, sets the OSPF network type to point-to-point,
+allowing adjacencies to form over interfaces without explicitly assigned IP
+addresses. This *must* be enabled for interfaces where there is no address
+configured in the IPv4 column.
+
+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_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

* [pve-devel] applied: [PATCH pve-cluster v2 1/1] cluster: add sdn fabrics config files
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-cluster v2 1/1] cluster: add sdn fabrics config files Gabriel Goller
@ 2025-04-04 17:03   ` Thomas Lamprecht
  0 siblings, 0 replies; 76+ messages in thread
From: Thomas Lamprecht @ 2025-04-04 17:03 UTC (permalink / raw)
  To: Proxmox VE development discussion, Gabriel Goller

Am 04.04.25 um 18:28 schrieb Gabriel Goller:
> Add the sdn fabrics config files. These are split into two, as we
> currently support two fabric types: ospf and openfabric. They hold the
> whole configuration for the respective protocols. They are read and
> written by pve-network.
> 
> Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
> ---
>  src/PVE/Cluster.pm  | 2 ++
>  src/pmxcfs/status.c | 2 ++
>  2 files changed, 4 insertions(+)
> 
>

applied this one already, thanks!


_______________________________________________
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

* Re: [pve-devel] [PATCH pve-access-control v2 1/1] permissions: add ACL paths for SDN fabrics
  2025-04-04 16:28 ` [pve-devel] [PATCH pve-access-control v2 1/1] permissions: add ACL paths for SDN fabrics Gabriel Goller
@ 2025-04-04 17:20   ` Thomas Lamprecht
  2025-04-07  7:24     ` Fabian Grünbichler
  0 siblings, 1 reply; 76+ messages in thread
From: Thomas Lamprecht @ 2025-04-04 17:20 UTC (permalink / raw)
  To: Proxmox VE development discussion, Gabriel Goller, Stefan Hanreich

Am 04.04.25 um 18:28 schrieb Gabriel Goller:
> From: Stefan Hanreich <s.hanreich@proxmox.com>

Missing a commit message, ACL is something that might profit from
providing the thoughts behind this, even if it's probably quite
clear for you.

> 
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> Signed-off-by: Gabriel Goller <g.goller@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 47f2d38b09c7..7b2dae35448d 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/(openfabric|ospf)/[[:alnum:]]+

So, without looking at the implementation, fabrics have the IDs unique
per sub-type? Could maybe also share an ID space, less confusion
potential, but naturally also less flexibility – what do you think?





_______________________________________________
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

* Re: [pve-devel] [PATCH pve-access-control v2 1/1] permissions: add ACL paths for SDN fabrics
  2025-04-04 17:20   ` Thomas Lamprecht
@ 2025-04-07  7:24     ` Fabian Grünbichler
  2025-04-07  8:12       ` Thomas Lamprecht
  0 siblings, 1 reply; 76+ messages in thread
From: Fabian Grünbichler @ 2025-04-07  7:24 UTC (permalink / raw)
  To: Gabriel Goller, Proxmox VE development discussion, Stefan Hanreich

On April 4, 2025 7:20 pm, Thomas Lamprecht wrote:
> Am 04.04.25 um 18:28 schrieb Gabriel Goller:
>> From: Stefan Hanreich <s.hanreich@proxmox.com>
> 
> Missing a commit message, ACL is something that might profit from
> providing the thoughts behind this, even if it's probably quite
> clear for you.
> 
>> 
>> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
>> Signed-off-by: Gabriel Goller <g.goller@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 47f2d38b09c7..7b2dae35448d 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/(openfabric|ospf)/[[:alnum:]]+
> 
> So, without looking at the implementation, fabrics have the IDs unique
> per sub-type? Could maybe also share an ID space, less confusion
> potential, but naturally also less flexibility – what do you think?

they share a section config (and thus ID-space), so I guess we could
skip the sub-type component here if we intend to keep it like that
(forever ;)).

unless there is a (current or future) use case for handing out blanket
permissions for one specific fabric type, but not the other(s)?


_______________________________________________
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

* Re: [pve-devel] [PATCH pve-access-control v2 1/1] permissions: add ACL paths for SDN fabrics
  2025-04-07  7:24     ` Fabian Grünbichler
@ 2025-04-07  8:12       ` Thomas Lamprecht
  2025-04-07  8:51         ` Stefan Hanreich
  0 siblings, 1 reply; 76+ messages in thread
From: Thomas Lamprecht @ 2025-04-07  8:12 UTC (permalink / raw)
  To: Proxmox VE development discussion, Fabian Grünbichler,
	Gabriel Goller, Stefan Hanreich

Am 07.04.25 um 09:24 schrieb Fabian Grünbichler:
> On April 4, 2025 7:20 pm, Thomas Lamprecht wrote:
>> So, without looking at the implementation, fabrics have the IDs unique
>> per sub-type? Could maybe also share an ID space, less confusion
>> potential, but naturally also less flexibility – what do you think?
> 
> they share a section config (and thus ID-space), so I guess we could

Hmm, ok no real value for allowing a user use-access on such a fabric
(for now), and not sure if it is useful to allow certain admins to create
an openfabric fabric but not an ospf one (if the implementation even
uses such fine-grained checks)

> skip the sub-type component here if we intend to keep it like that
> (forever ;)).
> 
> unless there is a (current or future) use case for handing out blanket
> permissions for one specific fabric type, but not the other(s)?

FWIW, we could then split it with a new root prefix and transform the
ACLs on migration in a two-step process, first copy all to new respective
sub paths and then drop the old ones once all nodes have been upgraded.
Might have some hairy details to figure out but nothing really
impossible I think, at least not that much worse than splitting the
configs themselves.

So think we can ignore potential migrations woes of an ID split here,
that's gonna be a bit painful no matter what, and rather focus on the
actual relevant use cases now. If the config shares ID it's a strong
indication that the ACLs should be shared too. Otherwise, it might
make sense to have the configurations use different ID spaces.

Gabriel, Stefan: your input please ;-)


_______________________________________________
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

* Re: [pve-devel] [PATCH pve-access-control v2 1/1] permissions: add ACL paths for SDN fabrics
  2025-04-07  8:12       ` Thomas Lamprecht
@ 2025-04-07  8:51         ` Stefan Hanreich
  2025-04-07  9:27           ` Fabian Grünbichler
  2025-04-07  9:34           ` Thomas Lamprecht
  0 siblings, 2 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-04-07  8:51 UTC (permalink / raw)
  To: Thomas Lamprecht, Proxmox VE development discussion,
	Fabian Grünbichler, Gabriel Goller

On 4/7/25 10:12, Thomas Lamprecht wrote:
> Am 07.04.25 um 09:24 schrieb Fabian Grünbichler:
>> On April 4, 2025 7:20 pm, Thomas Lamprecht wrote:
>>> So, without looking at the implementation, fabrics have the IDs unique
>>> per sub-type? Could maybe also share an ID space, less confusion
>>> potential, but naturally also less flexibility – what do you think?
>>
>> they share a section config (and thus ID-space), so I guess we could

There's one section config file per fabric, so its ID is unique per
protocol. We'd need to load *all* configuration files everytime we
change one configuration file (at least when adding a fabric) so we can
validate unique IDs across all fabric types.

Since we have plans of moving over the existing IS-IS + BGP controllers,
as well as a new WireGuard fabric, we'd have to load and parse 5
different configuration files for cross-validation then.

> Hmm, ok no real value for allowing a user use-access on such a fabric
> (for now), and not sure if it is useful to allow certain admins to create
> an openfabric fabric but not an ospf one (if the implementation even
> uses such fine-grained checks)

I think that in practice, admins would create the fabrics and then only
hand out permissions to edit that specific fabric, without handing out
the permissions to create fabrics at all.

It might make sense for Wireguard (where the possible implications
aren't nearly as bad as with e.g. BGP), but even then I think my
previous point applies that you probably don't want to hand out blanket
permissions for a whole subtype, in case you want to make heavy use of ACLs.

The current schema is more of a direct result of how the section config
is structured, since IDs can be duplicated across different protocols,
so you need the additional distinguisher in the ACL path.


_______________________________________________
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

* Re: [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics
  2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
                   ` (56 preceding siblings ...)
  2025-04-04 16:29 ` [pve-devel] [PATCH pve-docs v2 1/1] fabrics: add initial documentation for sdn fabrics Gabriel Goller
@ 2025-04-07  8:53 ` Friedrich Weber
  2025-04-07  9:39   ` Stefan Hanreich
  57 siblings, 1 reply; 76+ messages in thread
From: Friedrich Weber @ 2025-04-07  8:53 UTC (permalink / raw)
  To: Proxmox VE development discussion, Gabriel Goller

On 04/04/2025 18:28, Gabriel Goller wrote:
> This series allows the user to add fabrics such as OpenFabric and OSPF over
> their clusters.
> 
> This series relies on: 
> https://lore.proxmox.com/pve-devel/20250404135522.2603272-1-s.hanreich@proxmox.com/T/#mf4cf46c066d856cea819ac3e79d115a290f47466

Thanks for the v2, I like this feature a lot!

Unfortunately, one problem I noticed while testing this is that it may
break pre-existing FRR configs (such as full-mesh Ceph clusters set up
according to [1]) when making seemingly unrelated SDN changes. I already
quickly discussed this with Stefan, posting here in case others have
input as well.

Steps to reproduce:

- on PVE 8.3 (without these patches), set up Ceph full mesh with
OpenFabric as described in [1], includes custom /etc/frr/frr.conf
- also use some SDN feature, e.g. a VLAN zone with a Vnet
- install patched packages, systemctl restart pveproxy pvedaemon
- make a fabric-unrelated change in the SDN config, e.g. change tag of
the VLAN zone Vnet
- apply SDN config

=>
SDN stack writes out a nearly-empty /etc/frr/frr.conf on all nodes and
thus takes down the full mesh:

# cat /etc/frr/frr.conf
frr version 10.2.1
frr defaults datacenter
hostname fabric159
log syslog informational
service integrated-vtysh-config
!
!
line vty

It seems to also disable the fabricd daemon in /etc/frr/daemons:

# grep fabric /etc/frr/daemons
fabricd=no
fabricd_options="-A 127.0.0.1 --dummy_as_loopback"
# vtysh -c 'show openfabric route'
fabricd is not running

It makes sense that one cannot use both our fabrics integration and
custom FRR configs, but the above SDN config change is not related to
fabrics, so we should probably avoid touching the frr.conf if possible.
The wiki article [1] does warn that the full mesh doesn't work in
combination with EVPN, but unfortunately doesn't mention an inherent
incompatibility with the SDN stack as a whole.

[1]
https://pve.proxmox.com/wiki/Full_Mesh_Network_for_Ceph_Server#Routed_Setup_(with_Fallback)


_______________________________________________
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

* Re: [pve-devel] [PATCH pve-access-control v2 1/1] permissions: add ACL paths for SDN fabrics
  2025-04-07  8:51         ` Stefan Hanreich
@ 2025-04-07  9:27           ` Fabian Grünbichler
  2025-04-07  9:44             ` Stefan Hanreich
  2025-04-11 11:12             ` Stefan Hanreich
  2025-04-07  9:34           ` Thomas Lamprecht
  1 sibling, 2 replies; 76+ messages in thread
From: Fabian Grünbichler @ 2025-04-07  9:27 UTC (permalink / raw)
  To: Stefan Hanreich, Thomas Lamprecht,
	Proxmox VE development discussion, Gabriel Goller


> Stefan Hanreich <s.hanreich@proxmox.com> hat am 07.04.2025 10:51 CEST geschrieben:
> 
>  
> On 4/7/25 10:12, Thomas Lamprecht wrote:
> > Am 07.04.25 um 09:24 schrieb Fabian Grünbichler:
> >> On April 4, 2025 7:20 pm, Thomas Lamprecht wrote:
> >>> So, without looking at the implementation, fabrics have the IDs unique
> >>> per sub-type? Could maybe also share an ID space, less confusion
> >>> potential, but naturally also less flexibility – what do you think?
> >>
> >> they share a section config (and thus ID-space), so I guess we could
> 
> There's one section config file per fabric, so its ID is unique per
> protocol. We'd need to load *all* configuration files everytime we
> change one configuration file (at least when adding a fabric) so we can
> validate unique IDs across all fabric types.

mea culpa, must have mixed that up with something else (probably the
controllers :-/). would it make sense to merge them given the similarities?

> Since we have plans of moving over the existing IS-IS + BGP controllers,

those already have their own ACL paths, which makes this a bit messy then..
or does that mean the old controller config and endpoints will be removed
in favor of their counterparts in fabrics?

> as well as a new WireGuard fabric, we'd have to load and parse 5
> different configuration files for cross-validation then.
> 
> > Hmm, ok no real value for allowing a user use-access on such a fabric
> > (for now), and not sure if it is useful to allow certain admins to create
> > an openfabric fabric but not an ospf one (if the implementation even
> > uses such fine-grained checks)
> 
> I think that in practice, admins would create the fabrics and then only
> hand out permissions to edit that specific fabric, without handing out
> the permissions to create fabrics at all.
> 
> It might make sense for Wireguard (where the possible implications
> aren't nearly as bad as with e.g. BGP), but even then I think my
> previous point applies that you probably don't want to hand out blanket
> permissions for a whole subtype, in case you want to make heavy use of ACLs.
> 
> The current schema is more of a direct result of how the section config
> is structured, since IDs can be duplicated across different protocols,
> so you need the additional distinguisher in the ACL path.


_______________________________________________
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

* Re: [pve-devel] [PATCH pve-access-control v2 1/1] permissions: add ACL paths for SDN fabrics
  2025-04-07  8:51         ` Stefan Hanreich
  2025-04-07  9:27           ` Fabian Grünbichler
@ 2025-04-07  9:34           ` Thomas Lamprecht
  2025-04-07 10:08             ` Stefan Hanreich
  1 sibling, 1 reply; 76+ messages in thread
From: Thomas Lamprecht @ 2025-04-07  9:34 UTC (permalink / raw)
  To: Proxmox VE development discussion, Stefan Hanreich,
	Fabian Grünbichler, Gabriel Goller

Am 07.04.25 um 10:51 schrieb Stefan Hanreich:
> On 4/7/25 10:12, Thomas Lamprecht wrote:
>> Am 07.04.25 um 09:24 schrieb Fabian Grünbichler:
>>> On April 4, 2025 7:20 pm, Thomas Lamprecht wrote:
>>>> So, without looking at the implementation, fabrics have the IDs unique
>>>> per sub-type? Could maybe also share an ID space, less confusion
>>>> potential, but naturally also less flexibility – what do you think?
>>>
>>> they share a section config (and thus ID-space), so I guess we could
> 
> There's one section config file per fabric, so its ID is unique per
> protocol. We'd need to load *all* configuration files everytime we
> change one configuration file (at least when adding a fabric) so we can
> validate unique IDs across all fabric types.

I faintly remember some lunch talks, but what was the story behind the
per type configs again? The per-node mappings?

Can you post some sample configs for my convenience, or point to them
if these patches here contain some (e.g., for testing), so that I (or
someone other core maintainer) can take a closer look at this part.


> Since we have plans of moving over the existing IS-IS + BGP controllers,
> as well as a new WireGuard fabric, we'd have to load and parse 5
> different configuration files for cross-validation then.
> 
>> Hmm, ok no real value for allowing a user use-access on such a fabric
>> (for now), and not sure if it is useful to allow certain admins to create
>> an openfabric fabric but not an ospf one (if the implementation even
>> uses such fine-grained checks)
> 
> I think that in practice, admins would create the fabrics and then only
> hand out permissions to edit that specific fabric, without handing out
> the permissions to create fabrics at all.
> 
> It might make sense for Wireguard (where the possible implications
> aren't nearly as bad as with e.g. BGP), but even then I think my
> previous point applies that you probably don't want to hand out blanket
> permissions for a whole subtype, in case you want to make heavy use of ACLs.
> 
> The current schema is more of a direct result of how the section config
> is structured, since IDs can be duplicated across different protocols,
> so you need the additional distinguisher in the ACL path.

With the configs you describe it probably indeed make sense to have
per-type ACL base paths, that said, I'd like to revisit the design
again with more time at hand; IMO this is a bit to short notice and
to big bags to hold for us to rush it in now.

As of now I'd pretty certain that I'll only get around taking a
closer look post releases, so this might be probably be a better fit
for the next major release in a two to three months. Or would you
prefer getting this in now to acquire feedback but then having to
potentially rework the config/acl including migrations in a bigger
way? Slapping some tech-preview label on it might set expectations
roughly right for users in that case. I cannot give you a promise
here but if you think it should go in and promise me to work on
such a transformation then I will set some time aside to take a
serious look today. But please consider whether a new feature at
the end of the new feature release cycle is worth the effort for
all of us. 

A side-benefit of waiting might be that you get around to also
move bgp and is-is over too until then, ensuring the config/api
design is really a good fit for that too.


_______________________________________________
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

* Re: [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics
  2025-04-07  8:53 ` [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Friedrich Weber
@ 2025-04-07  9:39   ` Stefan Hanreich
  0 siblings, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-04-07  9:39 UTC (permalink / raw)
  To: Friedrich Weber, Proxmox VE development discussion, Gabriel Goller

On 4/7/25 10:53, Friedrich Weber wrote:
> On 04/04/2025 18:28, Gabriel Goller wrote:
>> This series allows the user to add fabrics such as OpenFabric and OSPF over
>> their clusters.
>>
>> This series relies on: 
>> https://lore.proxmox.com/pve-devel/20250404135522.2603272-1-s.hanreich@proxmox.com/T/#mf4cf46c066d856cea819ac3e79d115a290f47466
> 
> Thanks for the v2, I like this feature a lot!
> 
> Unfortunately, one problem I noticed while testing this is that it may
> break pre-existing FRR configs (such as full-mesh Ceph clusters set up
> according to [1]) when making seemingly unrelated SDN changes. I already
> quickly discussed this with Stefan, posting here in case others have
> input as well.
> 
> Steps to reproduce:
> 
> - on PVE 8.3 (without these patches), set up Ceph full mesh with
> OpenFabric as described in [1], includes custom /etc/frr/frr.conf
> - also use some SDN feature, e.g. a VLAN zone with a Vnet
> - install patched packages, systemctl restart pveproxy pvedaemon
> - make a fabric-unrelated change in the SDN config, e.g. change tag of
> the VLAN zone Vnet
> - apply SDN config
> 
> =>
> SDN stack writes out a nearly-empty /etc/frr/frr.conf on all nodes and
> thus takes down the full mesh:
> 
> # cat /etc/frr/frr.conf
> frr version 10.2.1
> frr defaults datacenter
> hostname fabric159
> log syslog informational
> service integrated-vtysh-config
> !
> !
> line vty
> 
> It seems to also disable the fabricd daemon in /etc/frr/daemons:
> 
> # grep fabric /etc/frr/daemons
> fabricd=no
> fabricd_options="-A 127.0.0.1 --dummy_as_loopback"
> # vtysh -c 'show openfabric route'
> fabricd is not running
> 
> It makes sense that one cannot use both our fabrics integration and
> custom FRR configs, but the above SDN config change is not related to
> fabrics, so we should probably avoid touching the frr.conf if possible.
> The wiki article [1] does warn that the full mesh doesn't work in
> combination with EVPN, but unfortunately doesn't mention an inherent
> incompatibility with the SDN stack as a whole.

For context: The initial issue here was that we previously did *not*
re-write the FRR configuration when you had an EVPN controller and
deleted it afterwards. So the FRR configuration actually lingered around
after deleting the EVPN controller.

That's because FRR config writing was bound to the EVPN controller. If
you didn't have one, the configuration wouldn't get written at all. In
my refactoring of the FRR config generation, I changed this to always
write the FRR config. That was intended to fix the bug mentioned above.


The mitigation I see is:

Read the previous running configuration before applying the new one.
Then, if the previous configuration contained any FRR-related entities
*or* the new configuration contains FRR-related entities: regenerate the
FRR config, otherwise leave as is. That would restore the previous
behavior and should fix this regression.

The only thing that would then change compared to before is that if you
*only* had an IS-IS and/or BGP controller before (which did not generate
any FRR configuration without an EVPN controller), reapplying with any
of those in your configuration will overwrite the full-mesh
configuration as well, since those cause a FRR configuration write as
well now.

We could further restrict it to specific FRR types (EVPN controller and
fabrics I'd say), but that would re-introduce the behavior mentioned
above where EVPN, BGP and IS-IS routers linger around when deleting an
EVPN controller (and having no fabrics).


_______________________________________________
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

* Re: [pve-devel] [PATCH pve-access-control v2 1/1] permissions: add ACL paths for SDN fabrics
  2025-04-07  9:27           ` Fabian Grünbichler
@ 2025-04-07  9:44             ` Stefan Hanreich
  2025-04-11 11:12             ` Stefan Hanreich
  1 sibling, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-04-07  9:44 UTC (permalink / raw)
  To: Fabian Grünbichler, Thomas Lamprecht,
	Proxmox VE development discussion, Gabriel Goller



On 4/7/25 11:27, Fabian Grünbichler wrote:
> 
>> Stefan Hanreich <s.hanreich@proxmox.com> hat am 07.04.2025 10:51 CEST geschrieben:
>>
>>  
>> On 4/7/25 10:12, Thomas Lamprecht wrote:
>>> Am 07.04.25 um 09:24 schrieb Fabian Grünbichler:
>>>> On April 4, 2025 7:20 pm, Thomas Lamprecht wrote:
>>>>> So, without looking at the implementation, fabrics have the IDs unique
>>>>> per sub-type? Could maybe also share an ID space, less confusion
>>>>> potential, but naturally also less flexibility – what do you think?
>>>>
>>>> they share a section config (and thus ID-space), so I guess we could
>>
>> There's one section config file per fabric, so its ID is unique per
>> protocol. We'd need to load *all* configuration files everytime we
>> change one configuration file (at least when adding a fabric) so we can
>> validate unique IDs across all fabric types.
> 
> mea culpa, must have mixed that up with something else (probably the
> controllers :-/). would it make sense to merge them given the similarities?

The fabric section itself and the interface keys (contained in the node
section) have very different properties (almost disjunct). That will
only get exacerbated when adding e.g. Wireguard which is even more
dissimilar as compared to OpenFabric/OSPF.

We thought about having only one configuration file and introducing a
section type per protocol and fabric / node combination, but that seemed
unwieldy which is why we decided against it.

>> Since we have plans of moving over the existing IS-IS + BGP controllers,
> 
> those already have their own ACL paths, which makes this a bit messy then..
> or does that mean the old controller config and endpoints will be removed
> in favor of their counterparts in fabrics?

Yes, the intention is to completely move them over. I already talked
shortly about it off-list with Thomas. Since we would need some form of
migration script there, we would move it over during that process.


_______________________________________________
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

* Re: [pve-devel] [PATCH pve-access-control v2 1/1] permissions: add ACL paths for SDN fabrics
  2025-04-07  9:34           ` Thomas Lamprecht
@ 2025-04-07 10:08             ` Stefan Hanreich
  2025-04-07 10:12               ` Thomas Lamprecht
  0 siblings, 1 reply; 76+ messages in thread
From: Stefan Hanreich @ 2025-04-07 10:08 UTC (permalink / raw)
  To: Thomas Lamprecht, Proxmox VE development discussion,
	Fabian Grünbichler, Gabriel Goller



On 4/7/25 11:34, Thomas Lamprecht wrote:
> Am 07.04.25 um 10:51 schrieb Stefan Hanreich:
>> On 4/7/25 10:12, Thomas Lamprecht wrote:
>>> Am 07.04.25 um 09:24 schrieb Fabian Grünbichler:
>>>> On April 4, 2025 7:20 pm, Thomas Lamprecht wrote:
>>>>> So, without looking at the implementation, fabrics have the IDs unique
>>>>> per sub-type? Could maybe also share an ID space, less confusion
>>>>> potential, but naturally also less flexibility – what do you think?
>>>>
>>>> they share a section config (and thus ID-space), so I guess we could
>>
>> There's one section config file per fabric, so its ID is unique per
>> protocol. We'd need to load *all* configuration files everytime we
>> change one configuration file (at least when adding a fabric) so we can
>> validate unique IDs across all fabric types.
> 
> I faintly remember some lunch talks, but what was the story behind the
> per type configs again? The per-node mappings?

Sorry, didn't see this email before sending the reply to Fabian:

Mostly the disjunct attributes in the fabric section, as well as the
interface key of the node section. OSPF and OpenFabric are relatively
similar, only differing in some protocol parameters. BGP and Wireguard
probably diverge even more and we wanted to avoid mixing too many
dissimilar sections.

Thinking more about it, I think we might have underexplored just putting
every type into one configuration file (as is the case with zones or
controllers for instance). I don't think it should be too hard to change
though with the current implementation.

> Can you post some sample configs for my convenience, or point to them
> if these patches here contain some (e.g., for testing), so that I (or
> someone other core maintainer) can take a closer look at this part.

There are some in the integration tests of proxmox-perl-rs, as well as
in the pve-network integration tests, but here they are for your
convenience (taken from my test cluster):


$ cat /etc/pve/sdn/fabrics/openfabric.cfg

fabric: evpn
	hello_interval 1
	loopback_prefix 172.20.30.0/24

node: evpn_deadeye
	fabric_id evpn
	interfaces name=ens19,ip=172.16.3.10/31
	node_id deadeye
	router_id 172.20.30.1

node: evpn_pathfinder
	fabric_id evpn
	interfaces name=ens19,ip=172.16.3.20/31
	node_id pathfinder
	router_id 172.20.30.2


$ cat /etc/pve/sdn/fabrics/ospf.cfg
fabric: ceph
	area 123
	loopback_prefix 172.20.3.0/24

node: ceph_deadeye
	fabric_id ceph
	interfaces name=ens20,unnumbered=true
	interfaces name=ens21,unnumbered=true
	node_id deadeye
	router_id 172.20.3.1

node: ceph_pathfinder
	fabric_id ceph
	interfaces name=ens20,unnumbered=true
	interfaces name=ens21,unnumbered=true
	node_id pathfinder
	router_id 172.20.3.2

node: ceph_raider
	fabric_id ceph
	interfaces name=ens20,unnumbered=true
	interfaces name=ens21,unnumbered=true
	node_id raider
	router_id 172.20.3.3


>> Since we have plans of moving over the existing IS-IS + BGP controllers,
>> as well as a new WireGuard fabric, we'd have to load and parse 5
>> different configuration files for cross-validation then.
>>
>>> Hmm, ok no real value for allowing a user use-access on such a fabric
>>> (for now), and not sure if it is useful to allow certain admins to create
>>> an openfabric fabric but not an ospf one (if the implementation even
>>> uses such fine-grained checks)
>>
>> I think that in practice, admins would create the fabrics and then only
>> hand out permissions to edit that specific fabric, without handing out
>> the permissions to create fabrics at all.
>>
>> It might make sense for Wireguard (where the possible implications
>> aren't nearly as bad as with e.g. BGP), but even then I think my
>> previous point applies that you probably don't want to hand out blanket
>> permissions for a whole subtype, in case you want to make heavy use of ACLs.
>>
>> The current schema is more of a direct result of how the section config
>> is structured, since IDs can be duplicated across different protocols,
>> so you need the additional distinguisher in the ACL path.
> 
> With the configs you describe it probably indeed make sense to have
> per-type ACL base paths, that said, I'd like to revisit the design
> again with more time at hand; IMO this is a bit to short notice and
> to big bags to hold for us to rush it in now.

Yes, I agree that we shouldn't rush this. I've thought about this
extensively today (and the weekend for that matter) and wanted to talk
to you about it today anyway.

After some consideration I strongly drifted towards not including it.
While I think this is functionally in a decent shape and I *really*
wanted to get this ready, I think we should take more time to carefully
consider and evaluate the underlying design. This is a fundamental
building block - particularly looking at further PDM integrations, so we
need to make sure to get this right. I don't think we can manage to do
this properly in a timely fashion, and I don't think this patch series
received the scrutiny it should given the importance and size.

> As of now I'd pretty certain that I'll only get around taking a
> closer look post releases, so this might be probably be a better fit
> for the next major release in a two to three months. Or would you
> prefer getting this in now to acquire feedback but then having to
> potentially rework the config/acl including migrations in a bigger
> way? Slapping some tech-preview label on it might set expectations
> roughly right for users in that case. I cannot give you a promise
> here but if you think it should go in and promise me to work on
> such a transformation then I will set some time aside to take a
> serious look today. But please consider whether a new feature at
> the end of the new feature release cycle is worth the effort for
> all of us. 

I think this might be for the better, this also has the potential to
affect existing EVPN setups as well with the refactoring portions. So
apart from discussing the design of the implementation, further
extensive testing would also be warranted imo (as evidenced by
Friedrich's report today).


> A side-benefit of waiting might be that you get around to also
> move bgp and is-is over too until then, ensuring the config/api
> design is really a good fit for that too.

I think so as well, same goes for a potential Wireguard integration.


_______________________________________________
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

* Re: [pve-devel] [PATCH pve-access-control v2 1/1] permissions: add ACL paths for SDN fabrics
  2025-04-07 10:08             ` Stefan Hanreich
@ 2025-04-07 10:12               ` Thomas Lamprecht
  2025-04-07 11:41                 ` Gilberto Ferreira via pve-devel
       [not found]                 ` <CAOKSTBsu8vrw8_nSu_LozwNwTc+ReTb6TEg3K_iM8uYh9oRRFg@mail.gmail.com>
  0 siblings, 2 replies; 76+ messages in thread
From: Thomas Lamprecht @ 2025-04-07 10:12 UTC (permalink / raw)
  To: Proxmox VE development discussion, Stefan Hanreich,
	Fabian Grünbichler, Gabriel Goller

Am 07.04.25 um 12:08 schrieb Stefan Hanreich:
> Yes, I agree that we shouldn't rush this. I've thought about this
> extensively today (and the weekend for that matter) and wanted to talk
> to you about it today anyway.
> 
> After some consideration I strongly drifted towards not including it.
> While I think this is functionally in a decent shape and I *really*
> wanted to get this ready, I think we should take more time to carefully
> consider and evaluate the underlying design. This is a fundamental
> building block - particularly looking at further PDM integrations, so we
> need to make sure to get this right. I don't think we can manage to do
> this properly in a timely fashion, and I don't think this patch series
> received the scrutiny it should given the importance and size.


Alright, thanks for your input here; I'll then hold-off from considering
this series for the time being, but let's definitively take a closer and
calmer look after the releases.


_______________________________________________
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

* Re: [pve-devel] [PATCH pve-access-control v2 1/1] permissions: add ACL paths for SDN fabrics
  2025-04-07 10:12               ` Thomas Lamprecht
@ 2025-04-07 11:41                 ` Gilberto Ferreira via pve-devel
       [not found]                 ` <CAOKSTBsu8vrw8_nSu_LozwNwTc+ReTb6TEg3K_iM8uYh9oRRFg@mail.gmail.com>
  1 sibling, 0 replies; 76+ messages in thread
From: Gilberto Ferreira via pve-devel @ 2025-04-07 11:41 UTC (permalink / raw)
  To: Proxmox VE development discussion; +Cc: Gilberto Ferreira

[-- Attachment #1: Type: message/rfc822, Size: 7336 bytes --]

From: Gilberto Ferreira <gilberto.nunes32@gmail.com>
To: Proxmox VE development discussion <pve-devel@lists.proxmox.com>
Subject: Re: [pve-devel] [PATCH pve-access-control v2 1/1] permissions: add ACL paths for SDN fabrics
Date: Mon, 7 Apr 2025 08:41:43 -0300
Message-ID: <CAOKSTBsu8vrw8_nSu_LozwNwTc+ReTb6TEg3K_iM8uYh9oRRFg@mail.gmail.com>

Hi there.
Sorry if I disrupt this thread in any way, but I need to know if this patch
series has something to do with frr services?
Currently, I am using Full Mesh Network for a customer, with 3 servers.
Sometimes, using SDN messes up with the configuration.
So, this patch has the goal to optimize the use of SDN combined with FRR
services?
Cheers
---


Gilberto Nunes Ferreira
(47) 99676-7530 - Whatsapp / Telegram






Em seg., 7 de abr. de 2025 às 07:13, Thomas Lamprecht <
t.lamprecht@proxmox.com> escreveu:

> Am 07.04.25 um 12:08 schrieb Stefan Hanreich:
> > Yes, I agree that we shouldn't rush this. I've thought about this
> > extensively today (and the weekend for that matter) and wanted to talk
> > to you about it today anyway.
> >
> > After some consideration I strongly drifted towards not including it.
> > While I think this is functionally in a decent shape and I *really*
> > wanted to get this ready, I think we should take more time to carefully
> > consider and evaluate the underlying design. This is a fundamental
> > building block - particularly looking at further PDM integrations, so we
> > need to make sure to get this right. I don't think we can manage to do
> > this properly in a timely fashion, and I don't think this patch series
> > received the scrutiny it should given the importance and size.
>
>
> Alright, thanks for your input here; I'll then hold-off from considering
> this series for the time being, but let's definitively take a closer and
> calmer look after the releases.
>
>
> _______________________________________________
> pve-devel mailing list
> pve-devel@lists.proxmox.com
> https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
>
>

[-- Attachment #2: Type: text/plain, Size: 160 bytes --]

_______________________________________________
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

* Re: [pve-devel] [PATCH pve-access-control v2 1/1] permissions: add ACL paths for SDN fabrics
       [not found]                 ` <CAOKSTBsu8vrw8_nSu_LozwNwTc+ReTb6TEg3K_iM8uYh9oRRFg@mail.gmail.com>
@ 2025-04-07 11:59                   ` Stefan Hanreich
  2025-04-07 12:22                     ` Gilberto Ferreira via pve-devel
  0 siblings, 1 reply; 76+ messages in thread
From: Stefan Hanreich @ 2025-04-07 11:59 UTC (permalink / raw)
  To: Gilberto Ferreira, Proxmox VE development discussion

On 4/7/25 13:41, Gilberto Ferreira wrote:
> Hi there.
> Sorry if I disrupt this thread in any way, but I need to know if this patch
> series has something to do with frr services?
> Currently, I am using Full Mesh Network for a customer, with 3 servers.
> Sometimes, using SDN messes up with the configuration.
> So, this patch has the goal to optimize the use of SDN combined with FRR
> services?

Yes, it is intended to provide UI integration for several routing
protocols, such as OpenFabric. This would enable, among other things,
managing full-mesh Ceph setups from the UI and finally make it possible
to use those setups alongside EVPN without any interference.


_______________________________________________
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

* Re: [pve-devel] [PATCH pve-access-control v2 1/1] permissions: add ACL paths for SDN fabrics
  2025-04-07 11:59                   ` Stefan Hanreich
@ 2025-04-07 12:22                     ` Gilberto Ferreira via pve-devel
  0 siblings, 0 replies; 76+ messages in thread
From: Gilberto Ferreira via pve-devel @ 2025-04-07 12:22 UTC (permalink / raw)
  To: Stefan Hanreich; +Cc: Gilberto Ferreira, Proxmox VE development discussion

[-- Attachment #1: Type: message/rfc822, Size: 6761 bytes --]

From: Gilberto Ferreira <gilberto.nunes32@gmail.com>
To: Stefan Hanreich <s.hanreich@proxmox.com>
Cc: "Proxmox VE development discussion" <pve-devel@lists.proxmox.com>, "Fabian Grünbichler" <f.gruenbichler@proxmox.com>, "Gabriel Goller" <g.goller@proxmox.com>
Subject: Re: [pve-devel] [PATCH pve-access-control v2 1/1] permissions: add ACL paths for SDN fabrics
Date: Mon, 7 Apr 2025 09:22:31 -0300
Message-ID: <CAOKSTBtUhWwC_ZfkWvFaVN8LiaMHbXOjK=PDpHvoV2WAoPEpyQ@mail.gmail.com>

Oh... Thas is a very welcome feature.
Thanks a lot.

Cheers
---


Gilberto Nunes Ferreira
+55 (47) 99676-7530 - Whatsapp / Telegram






Em seg., 7 de abr. de 2025 às 08:59, Stefan Hanreich <s.hanreich@proxmox.com>
escreveu:

> On 4/7/25 13:41, Gilberto Ferreira wrote:
> > Hi there.
> > Sorry if I disrupt this thread in any way, but I need to know if this
> patch
> > series has something to do with frr services?
> > Currently, I am using Full Mesh Network for a customer, with 3 servers.
> > Sometimes, using SDN messes up with the configuration.
> > So, this patch has the goal to optimize the use of SDN combined with FRR
> > services?
>
> Yes, it is intended to provide UI integration for several routing
> protocols, such as OpenFabric. This would enable, among other things,
> managing full-mesh Ceph setups from the UI and finally make it possible
> to use those setups alongside EVPN without any interference.
>
>

[-- Attachment #2: Type: text/plain, Size: 160 bytes --]

_______________________________________________
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

* Re: [pve-devel] [PATCH pve-access-control v2 1/1] permissions: add ACL paths for SDN fabrics
  2025-04-07  9:27           ` Fabian Grünbichler
  2025-04-07  9:44             ` Stefan Hanreich
@ 2025-04-11 11:12             ` Stefan Hanreich
  2025-04-11 11:14               ` Stefan Hanreich
  2025-04-11 16:51               ` Stefan Hanreich
  1 sibling, 2 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-04-11 11:12 UTC (permalink / raw)
  To: Fabian Grünbichler, Thomas Lamprecht,
	Proxmox VE development discussion, Gabriel Goller

On 4/7/25 11:27, Fabian Grünbichler wrote:
> 
>> Stefan Hanreich <s.hanreich@proxmox.com> hat am 07.04.2025 10:51 CEST geschrieben:
>>
>>  
>> On 4/7/25 10:12, Thomas Lamprecht wrote:
>>> Am 07.04.25 um 09:24 schrieb Fabian Grünbichler:
>>>> On April 4, 2025 7:20 pm, Thomas Lamprecht wrote:
>>>>> So, without looking at the implementation, fabrics have the IDs unique
>>>>> per sub-type? Could maybe also share an ID space, less confusion
>>>>> potential, but naturally also less flexibility – what do you think?
>>>>
>>>> they share a section config (and thus ID-space), so I guess we could
>>
>> There's one section config file per fabric, so its ID is unique per
>> protocol. We'd need to load *all* configuration files everytime we
>> change one configuration file (at least when adding a fabric) so we can
>> validate unique IDs across all fabric types.
> 
> mea culpa, must have mixed that up with something else (probably the
> controllers :-/). would it make sense to merge them given the similarities?

Re-visiting this again, I took some time to write about the problem in
greater detail and present some options. I'd really like to get the
ball rolling on this discussion sooner than later, since otherwise
it's hard to proceed with this patch series. This is a short overview
for you to get a better idea, but it might make sense to discuss this
in greater detail in person.

Overview
========

We have multiple types of fabrics:
OpenFabric, OSPF and more in the future.

We have the following hierarchy of models:
Fabric -> Node -> Interface

Some constraints for the models:
* There can be more than one fabric of a given type
* A fabric contains n nodes, a node contains n interfaces.
* A node can be part of multiple fabrics, but one interface of a node
  can only be a part of one fabric.
* Each of the three models has its own properties, that vary depending
  on the type of fabric

Some fabric specific constraints can apply as well, for instance:
* the IP prefixes of fabrics should not overlap
* the IP of a node should be unique within a fabric
* an OSPF interface can either have an IP or be unnumbered
* OSPFv2 supports only v4, OpenFabric supports both

Ideally, we'd like to validate all those constraints when modifying the
configuration via the API or loading it.


The key issues we ran into:
* The type of a section can only be uniquely identified together with
  the protocol (e.g. ospf_node)
* A node section can only be uniquely identified together with the id
  of the fabric that it belongs to (a node can occur in multiple
  fabrics)
* 3 layers is one too much to cram into one section imo, so we have to
  at least split fabrics and nodes into two sections
* parsing the type/id of a section into multiple fields is a bit
  clunky with the section-config implementation. Ideally we'd like to
  not have to do this separately at every site (rust, perl, frontend),
  but once when reading.
* ideally, validation of *all* the constraints laid out above


Solutions
=========

#1: Storing everything in one file
----------------------------------

  # fabrics.cfg
  <type>_(node|fabric): <fabricid>[_<nodeid>]

+ fixed amount of files
+ validation is simple
+ API/ACL paths are simple
+ related entities are in the same file
- composite section types
- composite section ids
- complex API definitions
- file can grow quite large


#2: One file per fabric type (current solution)
-----------------------------------------------

  # <type>.cfg
  fabric: <fabricid>
  node: <fabricid>_<nodeid>

+ simple section types
+ simple API definitions
+ related entities are in the same file
- API/ACL paths are more complex
- composite section ids
- variable number of files
- validation requires reading all files


#3: One file per level in the hierarchy
---------------------------------------

  # fabric.cfg
  <type>: <fabricid>

  # node.cfg
  <type>: <fabricid>_<nodeid>

+ simple section types
+ fixed amount of files
+ API/ACL paths are simple
- related entities are in different files
- complex API definitions
- composite section ids
- validation requires reading both files


#4: One file per fabric
-----------------------

  # <fabricid>.cfg
  fabric: <fabricid>
  node: <nodeid>

+ simple types
+ simple ids
+ API/ACL paths are simple
+ related entities are in the same file
- complex API definitions
- variable number of files
- information about type has to be encoded somewhere (directory?)
- validation requires reading all files


Imo, #2 and #3 seem like the contenders. #3 is what we use for zones /
vnets / subnets, and it is . #2 makes API definitions a lot nicer,
since we have exactly one schema per API endpoint, which also makes
the generated API types in Rust a lot nicer. With #3 we currently
generate two structs where basically every field is an Option<T>. The
solution for this could be using oneOf across the whole API, but afaik
that is not implemented in Schema2Rust currently. Alternatively, we
could pop our own abstraction models on top of the generated types and
convert from / to them when using the API in PDM, but the real
solution is implementing oneOf support in Schema2Rust.



_______________________________________________
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

* Re: [pve-devel] [PATCH pve-access-control v2 1/1] permissions: add ACL paths for SDN fabrics
  2025-04-11 11:12             ` Stefan Hanreich
@ 2025-04-11 11:14               ` Stefan Hanreich
  2025-04-11 16:51               ` Stefan Hanreich
  1 sibling, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-04-11 11:14 UTC (permalink / raw)
  To: Fabian Grünbichler, Thomas Lamprecht,
	Proxmox VE development discussion, Gabriel Goller

Hit send too early when editing

On 4/11/25 13:12, Stefan Hanreich wrote:

> Imo, #2 and #3 seem like the contenders. #3 is what we use for zones /
> vnets / subnets, and it is . #2 makes API definitions a lot nicer,

[..] it is established in SDN already and works there.

> since we have exactly one schema per API endpoint, which also makes
> the generated API types in Rust a lot nicer. With #3 we currently
> generate two structs where basically every field is an Option<T>. The
> solution for this could be using oneOf across the whole API, but afaik
> that is not implemented in Schema2Rust currently. Alternatively, we
> could pop our own abstraction models on top of the generated types and
> convert from / to them when using the API in PDM, but the real
> solution is implementing oneOf support in Schema2Rust.



_______________________________________________
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

* Re: [pve-devel] [PATCH pve-access-control v2 1/1] permissions: add ACL paths for SDN fabrics
  2025-04-11 11:12             ` Stefan Hanreich
  2025-04-11 11:14               ` Stefan Hanreich
@ 2025-04-11 16:51               ` Stefan Hanreich
  1 sibling, 0 replies; 76+ messages in thread
From: Stefan Hanreich @ 2025-04-11 16:51 UTC (permalink / raw)
  To: Fabian Grünbichler, Thomas Lamprecht,
	Proxmox VE development discussion, Gabriel Goller

On 4/11/25 1:12 PM, Stefan Hanreich wrote:
> On 4/7/25 11:27, Fabian Grünbichler wrote:
>>
>>> Stefan Hanreich <s.hanreich@proxmox.com> hat am 07.04.2025 10:51 CEST geschrieben:
>>>
>>>   
>>> On 4/7/25 10:12, Thomas Lamprecht wrote:
>>>> Am 07.04.25 um 09:24 schrieb Fabian Grünbichler:
>>>>> On April 4, 2025 7:20 pm, Thomas Lamprecht wrote:
>>>>>> So, without looking at the implementation, fabrics have the IDs unique
>>>>>> per sub-type? Could maybe also share an ID space, less confusion
>>>>>> potential, but naturally also less flexibility – what do you think?
>>>>>
>>>>> they share a section config (and thus ID-space), so I guess we could
>>>
>>> There's one section config file per fabric, so its ID is unique per
>>> protocol. We'd need to load *all* configuration files everytime we
>>> change one configuration file (at least when adding a fabric) so we can
>>> validate unique IDs across all fabric types.
>>
>> mea culpa, must have mixed that up with something else (probably the
>> controllers :-/). would it make sense to merge them given the similarities?
> 
> Re-visiting this again, I took some time to write about the problem in
> greater detail and present some options. I'd really like to get the
> ball rolling on this discussion sooner than later, since otherwise
> it's hard to proceed with this patch series. This is a short overview
> for you to get a better idea, but it might make sense to discuss this
> in greater detail in person.
> 
> Overview
> ========
> 
> We have multiple types of fabrics:
> OpenFabric, OSPF and more in the future.
> 
> We have the following hierarchy of models:
> Fabric -> Node -> Interface
> 
> Some constraints for the models:
> * There can be more than one fabric of a given type
> * A fabric contains n nodes, a node contains n interfaces.
> * A node can be part of multiple fabrics, but one interface of a node
>    can only be a part of one fabric.
> * Each of the three models has its own properties, that vary depending
>    on the type of fabric
> 
> Some fabric specific constraints can apply as well, for instance:
> * the IP prefixes of fabrics should not overlap
> * the IP of a node should be unique within a fabric
> * an OSPF interface can either have an IP or be unnumbered
> * OSPFv2 supports only v4, OpenFabric supports both
> 
> Ideally, we'd like to validate all those constraints when modifying the
> configuration via the API or loading it.
> 
> 
> The key issues we ran into:
> * The type of a section can only be uniquely identified together with
>    the protocol (e.g. ospf_node)
> * A node section can only be uniquely identified together with the id
>    of the fabric that it belongs to (a node can occur in multiple
>    fabrics)
> * 3 layers is one too much to cram into one section imo, so we have to
>    at least split fabrics and nodes into two sections
> * parsing the type/id of a section into multiple fields is a bit
>    clunky with the section-config implementation. Ideally we'd like to
>    not have to do this separately at every site (rust, perl, frontend),
>    but once when reading.
> * ideally, validation of *all* the constraints laid out above
> 
> 
> Solutions
> =========
> 
> #1: Storing everything in one file
> ----------------------------------
> 
>    # fabrics.cfg
>    <type>_(node|fabric): <fabricid>[_<nodeid>]
> 
> + fixed amount of files
> + validation is simple
> + API/ACL paths are simple
> + related entities are in the same file
> - composite section types
> - composite section ids
> - complex API definitions
> - file can grow quite large
> 
> 
> #2: One file per fabric type (current solution)
> -----------------------------------------------
> 
>    # <type>.cfg
>    fabric: <fabricid>
>    node: <fabricid>_<nodeid>
> 
> + simple section types
> + simple API definitions
> + related entities are in the same file
> - API/ACL paths are more complex
> - composite section ids
> - variable number of files
> - validation requires reading all files
> 
> 
> #3: One file per level in the hierarchy
> ---------------------------------------
> 
>    # fabric.cfg
>    <type>: <fabricid>
> 
>    # node.cfg
>    <type>: <fabricid>_<nodeid>
> 
> + simple section types
> + fixed amount of files
> + API/ACL paths are simple
> - related entities are in different files
> - complex API definitions
> - composite section ids
> - validation requires reading both files
> 
> 
> #4: One file per fabric
> -----------------------
> 
>    # <fabricid>.cfg
>    fabric: <fabricid>
>    node: <nodeid>
> 
> + simple types
> + simple ids
> + API/ACL paths are simple
> + related entities are in the same file
> - complex API definitions
> - variable number of files
> - information about type has to be encoded somewhere (directory?)
> - validation requires reading all files
> 
> 
> Imo, #2 and #3 seem like the contenders. #3 is what we use for zones /
> vnets / subnets, and it is . #2 makes API definitions a lot nicer,
> since we have exactly one schema per API endpoint, which also makes
> the generated API types in Rust a lot nicer. With #3 we currently
> generate two structs where basically every field is an Option<T>. The
> solution for this could be using oneOf across the whole API, but afaik
> that is not implemented in Schema2Rust currently. Alternatively, we
> could pop our own abstraction models on top of the generated types and
> convert from / to them when using the API in PDM, but the real
> solution is implementing oneOf support in Schema2Rust.

Reusing the models directly on the PDM side would of course render the 
whole point wrt Schema2Rust moot.


_______________________________________________
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-04-11 16:52 UTC | newest]

Thread overview: 76+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-04-04 16:28 [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH proxmox v2 1/1] serde: add string_as_bool module for boolean string parsing Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 01/15] sdn-types: initial commit Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 02/15] frr: create proxmox-frr crate Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 03/15] frr: add common frr types Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 04/15] frr: add openfabric types Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 05/15] frr: add ospf types Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 06/15] frr: add route-map types Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 07/15] frr: add generic types over openfabric and ospf Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 08/15] frr: add serializer for all FRR types Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 09/15] ve-config: add common section-config types for OpenFabric and OSPF Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 10/15] ve-config: add openfabric section-config Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 11/15] ve-config: add ospf section-config Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 12/15] ve-config: add FRR conversion helpers for openfabric and ospf Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 13/15] ve-config: add validation for section-config Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 14/15] ve-config: add section-config to frr types conversion Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-ve-rs v2 15/15] ve-config: add integrations tests Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-perl-rs v2 1/7] perl-rs: sdn: initial fabric infrastructure Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-perl-rs v2 2/7] perl-rs: sdn: add CRUD helpers for OpenFabric fabric management Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-perl-rs v2 3/7] perl-rs: sdn: OpenFabric perlmod methods Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-perl-rs v2 4/7] perl-rs: sdn: implement Openfabric interface file generation Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-perl-rs v2 5/7] perl-rs: sdn: add CRUD helpers for OSPF fabric management Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-perl-rs v2 6/7] perl-rs: sdn: OSPF perlmod methods Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH proxmox-perl-rs v2 7/7] perl-rs: sdn: implement OSPF interface file configuration generation Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH pve-cluster v2 1/1] cluster: add sdn fabrics config files Gabriel Goller
2025-04-04 17:03   ` [pve-devel] applied: " Thomas Lamprecht
2025-04-04 16:28 ` [pve-devel] [PATCH pve-access-control v2 1/1] permissions: add ACL paths for SDN fabrics Gabriel Goller
2025-04-04 17:20   ` Thomas Lamprecht
2025-04-07  7:24     ` Fabian Grünbichler
2025-04-07  8:12       ` Thomas Lamprecht
2025-04-07  8:51         ` Stefan Hanreich
2025-04-07  9:27           ` Fabian Grünbichler
2025-04-07  9:44             ` Stefan Hanreich
2025-04-11 11:12             ` Stefan Hanreich
2025-04-11 11:14               ` Stefan Hanreich
2025-04-11 16:51               ` Stefan Hanreich
2025-04-07  9:34           ` Thomas Lamprecht
2025-04-07 10:08             ` Stefan Hanreich
2025-04-07 10:12               ` Thomas Lamprecht
2025-04-07 11:41                 ` Gilberto Ferreira via pve-devel
     [not found]                 ` <CAOKSTBsu8vrw8_nSu_LozwNwTc+ReTb6TEg3K_iM8uYh9oRRFg@mail.gmail.com>
2025-04-07 11:59                   ` Stefan Hanreich
2025-04-07 12:22                     ` Gilberto Ferreira via pve-devel
2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 01/19] sdn: fix value returned by pending_config Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 02/19] debian: add dependency to proxmox-perl-rs Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 03/19] fabrics: add fabrics module Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 04/19] refactor: controller: move frr methods into helper Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 05/19] frr: add new helpers for reloading frr configuration Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 06/19] controllers: implement new api for frr config generation Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 07/19] sdn: add frr config generation helper Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 08/19] test: isis: add test for standalone configuration Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 09/19] sdn: frr: add daemon status to frr helper Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 10/19] sdn: commit fabrics config to running configuration Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 11/19] fabrics: generate ifupdown configuration Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 12/19] api: fabrics: add common helpers Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 13/19] api: openfabric: add api endpoints Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 14/19] api: openfabric: add node endpoints Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 15/19] api: ospf: add fabric endpoints Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 16/19] api: ospf: add node endpoints Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 17/19] api: fabrics: add module / subfolder Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 18/19] test: fabrics: add test cases for ospf and openfabric + evpn Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH pve-network v2 19/19] frr: bump frr config version to 10.2.1 Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH pve-manager v2 01/11] api: use new generalized frr and etc network config helper functions Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH pve-manager v2 02/11] fabric: add common interface panel Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH pve-manager v2 03/11] fabric: add OpenFabric interface properties Gabriel Goller
2025-04-04 16:28 ` [pve-devel] [PATCH pve-manager v2 04/11] fabric: add OSPF " Gabriel Goller
2025-04-04 16:29 ` [pve-devel] [PATCH pve-manager v2 05/11] fabric: add generic node edit panel Gabriel Goller
2025-04-04 16:29 ` [pve-devel] [PATCH pve-manager v2 06/11] fabric: add generic fabric " Gabriel Goller
2025-04-04 16:29 ` [pve-devel] [PATCH pve-manager v2 07/11] fabric: add OpenFabric " Gabriel Goller
2025-04-04 16:29 ` [pve-devel] [PATCH pve-manager v2 08/11] fabric: add OSPF " Gabriel Goller
2025-04-04 16:29 ` [pve-devel] [PATCH pve-manager v2 09/11] fabrics: Add main FabricView Gabriel Goller
2025-04-04 16:29 ` [pve-devel] [PATCH pve-manager v2 10/11] utils: avoid line-break in pending changes message Gabriel Goller
2025-04-04 16:29 ` [pve-devel] [PATCH pve-manager v2 11/11] ui: permissions: add ACL paths for fabrics Gabriel Goller
2025-04-04 16:29 ` [pve-devel] [PATCH pve-gui-tests v2 1/1] pve: add sdn/fabrics screenshots Gabriel Goller
2025-04-04 16:29 ` [pve-devel] [PATCH pve-docs v2 1/1] fabrics: add initial documentation for sdn fabrics Gabriel Goller
2025-04-07  8:53 ` [pve-devel] [PATCH access-control/cluster/docs/gui-tests/manager/network/proxmox{, -ve-rs, -perl-rs} v2 00/57] Add SDN Fabrics Friedrich Weber
2025-04-07  9:39   ` 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