public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration
@ 2025-09-04  8:18 Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox v4 1/2] schema: use i64 for minimum / maximum / default integer values Stefan Hanreich
                   ` (32 more replies)
  0 siblings, 33 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

## Introduction

This patch series adds a new panel to the PDM that shows an overview of the
current state of all EVPN zones across all remotes. It includes two different
tree views:

* IP-VRFs: that shows the contents of all IP-VRFs (identified by their Route
  Target = ASN:VNI) across all remotes.
* Zones: that shows the contents of a specific zone on a specific remote.

For more information on the two tree views, consult the respective commits that
introduce the components.

The panel also allows users to create new Zones / VNets on multiple remotes
simultaneously by utilizing the new SDN locking functionality.

I have provided prebuilt packages on the share in the folder pdm-evpn.

This patch series requires the ParallelFetcher patch series from Lukas in order
to work.

## API

It introduces the following API endpoints on PDM:

/sdn
    GET /controllers - list the controllers of all remotes
    GET /zones - list the zones of all remotes
    POST /zones - create a zone on multiple remotes
    GET /vnets - list the vnets of all remotes
    POST /vnets - create a vnet on multiple remotes


## Additional remarks

This patch series contains some preparatory patches that are not directly
related to the implemented functionality:

* One fix for proxmox-schema so values that are larger than i32 can be used in
  the integer schema definition (required for e.g. 64-bit ASNs)
* Add JSONSchema to a lot of SDN API endpoints that were previously undocumented

I have sent them initially as separate patch series, but since they are a hard
requirement for this patch series I have merged all of them into one patch
series now. This way it is easier to keep track of the requirements.


## Open questions for reviewers

* The LockedSdnClient(s) are abstractions for locked SDN remotes. I'm still a
bit unsure about the design / implementation but for future features I will be
making more complex changes across multiple remotes so I figured an abstraction
for this will come in handy in the future.

I'd love some inputs / opinions on the API design as well as the general concept
of locking config -> making changes -> rolling back / applying.

I will work on a more sophisticated implementation utilizing tokio-specific
functions in the following days, but I wanted to get the patch series out now
and validate the API / general idea.

* We might wanna move the EvpnRouteTarget type out of the UI, even though it is
currently only used there.

* Should we introduce a caching mechanism for the SDN API calls?

I have shortly talked about this with @Lukas, but we decided against
implementing such a mechanism for now after some deliberation.

Showing outdated information is particularly problematic with configuration,
especially because the create dialogues rely on that information.

After creating a new zone / vnet we would have to hit the remotes anyway, in
order to be able to show the updated data immediately.

The downside is of course a long load time for the EVPN panel, as well as a long
load if even one of the remotes is not available.

For an initial release I think it is fine to go forward without caching and see
how it works out in practice based on reports from our users. Any input on this
matter would be greatly appreciated!


## Future Work
* show the output of the new status API calls created by Gabriel in the views.
* add a functionality for grouping remotes together, instead of implicitly
  grouping them based on ASN:VNI
* introduce a caching mechanism for the SDN API calls (?)
* integration tests with mocked SDN clients
* add some QoL to the UI (e.g expand/collapse all)


Huge thanks to @Lukas and @Dominik for helping me greatly on moving this patch
series forward the last few days!

## Changelog

Changes from v3 (Thanks @Shannon, @Wolfang):
* created dedicated verification functions for SDN IDs
* improved / fixed regex for SDN ids in the process
* improved API documentation for SDN zones in PVE
* use new verification functions in PDM types as well
* SDN client prints the correct remote when releasing the lock

Changes from v2:
* detect invalid response from rollback endpoint to gracefully handle unpatched
  libpve-network-api-perl
* use create_toolbar instead of implementing a whole component
* pass is_loading to Refresh button
* show spinner on initial load instead of empty trees
* improved default sorting order for remotes tree
* sort PveClients in LockedSdnClients to provide ordered output
* use HashSet for all list endpoints for deduplication and efficient filtering

Changes from v1:
* detect legacy PVE remotes without SDN locking API capability
* remove already applied patch
* parallelize list endpoints via Lukas' ParallelFetcher
* reversed toolbar / grid order in EVPN panel
* updated and improved commit messages
* added missing translation macro invocations
* replaced thread_local in components
* store columns in component to avoid re-creating them on update
* add better error message in add_zone/vnet dialogues if there is no
  controller / zone
* remove unused message from vrf/remote tree components
* use update_root_tree for restoring tree state
* moved EVPN above remotes in the main menu
* added instructions on how to unlock SDN configuration in cases of errors

Changes from RFC v2:
* rebased on top of current master
* improved error handling for the yew components considerably
* tinkered with column sizes in the remote view
* preserve collapsed state on refresh
* fix SDN ID schema definition
* improved EVPN icon
* moved task descriptions from yew-comp to pdm
* improved default sorting order for the remote view

Changes from RFC v1:
* overhauled the structure of the trees completely
  * split the initial tree view into two distinct tree views
  * changed the grouping of elements
  * improved and unified the terms used across all UI elements
* improved toolbar design
* removed the controller data table, since the tree views should now include
  that information
* improved locked SDN client and added a collection type for locked SDN clients
* improved error handling and logging considerably for the worker tasks


## Dependencies:
pbs-api-types depends on proxmox-schema
proxmox-api-types depends on proxmox-schema
proxmox-backup depends on proxmox-schema
proxmox-datacenter-manager depends on proxmox-schema

proxmox-api-types depends on pve-network
proxmox-datacenter-manager depends on proxmox-api-types
proxmox-datacenter-manager depends on pve-network

proxmox:

Stefan Hanreich (2):
  schema: use i64 for minimum / maximum / default integer values
  pbs-api-types: fix values for integer schemas

 pbs-api-types/src/datastore.rs  |  6 +++---
 proxmox-schema/src/de/mod.rs    |  3 +--
 proxmox-schema/src/de/verify.rs | 13 ++++++++-----
 proxmox-schema/src/schema.rs    | 18 +++++++++---------
 4 files changed, 21 insertions(+), 19 deletions(-)


proxmox-backup:

Stefan Hanreich (1):
  api: change integer schema parameters to i64

 pbs-tape/src/bin/pmt.rs           |  6 +++---
 proxmox-backup-client/src/main.rs |  2 +-
 pxar-bin/src/main.rs              |  6 +++---
 src/api2/backup/upload_chunk.rs   | 15 ++++++---------
 4 files changed, 13 insertions(+), 16 deletions(-)


pve-network:

Stefan Hanreich (6):
  sdn: api: return null for rollback / lock endpoints
  controllers: fix maximum value for ASN
  api: add state standard option
  api: controllers: update schema of endpoints
  api: vnets: update schema of endpoints
  api: zones: update schema of endpoints

 src/PVE/API2/Network/SDN.pm                   |   4 +
 src/PVE/API2/Network/SDN/Controllers.pm       | 116 +++++++++-
 src/PVE/API2/Network/SDN/Vnets.pm             |  92 +++++++-
 src/PVE/API2/Network/SDN/Zones.pm             | 204 ++++++++++++++++--
 src/PVE/Network/SDN.pm                        |  10 +
 src/PVE/Network/SDN/Controllers/BgpPlugin.pm  |   7 +-
 src/PVE/Network/SDN/Controllers/EvpnPlugin.pm |   2 +-
 src/PVE/Network/SDN/Controllers/IsisPlugin.pm |   6 +-
 src/PVE/Network/SDN/VnetPlugin.pm             |  21 +-
 src/PVE/Network/SDN/Zones/EvpnPlugin.pm       |  22 +-
 src/PVE/Network/SDN/Zones/QinQPlugin.pm       |   6 +-
 src/PVE/Network/SDN/Zones/VlanPlugin.pm       |   1 +
 src/PVE/Network/SDN/Zones/VxlanPlugin.pm      |  16 +-
 13 files changed, 459 insertions(+), 48 deletions(-)


proxmox-api-types:

Stefan Hanreich (6):
  sdn: add list/create zone endpoints
  sdn: add list/create vnet endpoints
  sdn: add list/create controller endpoints
  sdn: add sdn configuration locking endpoints
  tasks: add helper for querying successfully finished tasks
  sdn: add helpers for pending values

 pve-api-types/generate.pl            | 38 +++++++++++++++++++
 pve-api-types/src/lib.rs             |  1 +
 pve-api-types/src/sdn.rs             | 33 ++++++++++++++++
 pve-api-types/src/types/mod.rs       |  4 ++
 pve-api-types/src/types/verifiers.rs | 56 ++++++++++++++++++++++++++++
 5 files changed, 132 insertions(+)
 create mode 100644 pve-api-types/src/sdn.rs


proxmox-datacenter-manager:

Stefan Hanreich (15):
  server: add locked sdn client helpers
  ui: pve: sdn: add descriptions for sdn tasks
  api: sdn: add list_zones endpoint
  api: sdn: add create_zone endpoint
  api: sdn: add list_vnets endpoint
  api: sdn: add create_vnet endpoint
  api: sdn: add list_controllers endpoint
  ui: sdn: add EvpnRouteTarget type
  ui: sdn: add vnet icon
  ui: sdn: add view for showing evpn zones
  ui: sdn: add view for showing ip vrfs
  ui: sdn: add component for creating evpn vnets
  ui: sdn: add component for creatin evpn zones
  ui: sdn: add evpn overview panel
  ui: sdn: add evpn panel to main menu

 lib/pdm-api-types/Cargo.toml      |   2 +
 lib/pdm-api-types/src/lib.rs      |   2 +
 lib/pdm-api-types/src/sdn.rs      | 171 ++++++++++
 lib/pdm-client/src/lib.rs         |  61 ++++
 server/src/api/mod.rs             |   2 +
 server/src/api/sdn/controllers.rs | 114 +++++++
 server/src/api/sdn/mod.rs         |  17 +
 server/src/api/sdn/vnets.rs       | 180 +++++++++++
 server/src/api/sdn/zones.rs       | 206 +++++++++++++
 server/src/lib.rs                 |   1 +
 server/src/sdn_client.rs          | 432 ++++++++++++++++++++++++++
 ui/css/pdm.scss                   |  14 +-
 ui/images/icon-sdn-vnet.svg       |   6 +
 ui/src/lib.rs                     |   2 +
 ui/src/main_menu.rs               |  10 +
 ui/src/sdn/evpn/add_vnet.rs       | 313 +++++++++++++++++++
 ui/src/sdn/evpn/add_zone.rs       | 328 ++++++++++++++++++++
 ui/src/sdn/evpn/evpn_panel.rs     | 262 ++++++++++++++++
 ui/src/sdn/evpn/mod.rs            |  41 +++
 ui/src/sdn/evpn/remote_tree.rs    | 496 ++++++++++++++++++++++++++++++
 ui/src/sdn/evpn/vrf_tree.rs       | 409 ++++++++++++++++++++++++
 ui/src/sdn/mod.rs                 |   1 +
 ui/src/tasks.rs                   |   4 +
 23 files changed, 3073 insertions(+), 1 deletion(-)
 create mode 100644 lib/pdm-api-types/src/sdn.rs
 create mode 100644 server/src/api/sdn/controllers.rs
 create mode 100644 server/src/api/sdn/mod.rs
 create mode 100644 server/src/api/sdn/vnets.rs
 create mode 100644 server/src/api/sdn/zones.rs
 create mode 100644 server/src/sdn_client.rs
 create mode 100644 ui/images/icon-sdn-vnet.svg
 create mode 100644 ui/src/sdn/evpn/add_vnet.rs
 create mode 100644 ui/src/sdn/evpn/add_zone.rs
 create mode 100644 ui/src/sdn/evpn/evpn_panel.rs
 create mode 100644 ui/src/sdn/evpn/mod.rs
 create mode 100644 ui/src/sdn/evpn/remote_tree.rs
 create mode 100644 ui/src/sdn/evpn/vrf_tree.rs
 create mode 100644 ui/src/sdn/mod.rs


Summary over all repositories:
  49 files changed, 3698 insertions(+), 84 deletions(-)

-- 
Generated by git-murpp 0.8.0

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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH proxmox v4 1/2] schema: use i64 for minimum / maximum / default integer values
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04 10:03   ` [pdm-devel] applied: " Wolfgang Bumiller
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox v4 2/2] pbs-api-types: fix values for integer schemas Stefan Hanreich
                   ` (31 subsequent siblings)
  32 siblings, 1 reply; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

IntegerSchema used isize for the minimum / maximum parameters. On 32
bit targets (wasm for instance) API defintions with minimum / maximum
sizes outside the i32 range would fail.

This also changes the u64 deserialize to fail if it encounters a value
that cannot be represented as i64 instead of simply casting it to i64
and moving on.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-schema/src/de/mod.rs    |  3 +--
 proxmox-schema/src/de/verify.rs | 13 ++++++++-----
 proxmox-schema/src/schema.rs    | 18 +++++++++---------
 3 files changed, 18 insertions(+), 16 deletions(-)

diff --git a/proxmox-schema/src/de/mod.rs b/proxmox-schema/src/de/mod.rs
index eca835e3..1f14503e 100644
--- a/proxmox-schema/src/de/mod.rs
+++ b/proxmox-schema/src/de/mod.rs
@@ -173,8 +173,7 @@ impl<'de> de::Deserializer<'de> for SchemaDeserializer<'de, '_> {
                     .map_err(|_| Error::msg(format!("not a boolean: {:?}", self.input)))?,
             ),
             Schema::Integer(schema) => {
-                // FIXME: isize vs explicit i64, needs fixing in schema check_constraints api
-                let value: isize = self
+                let value: i64 = self
                     .input
                     .parse()
                     .map_err(|_| Error::msg(format!("not an integer: {:?}", self.input)))?;
diff --git a/proxmox-schema/src/de/verify.rs b/proxmox-schema/src/de/verify.rs
index 67a8c9e8..917f956b 100644
--- a/proxmox-schema/src/de/verify.rs
+++ b/proxmox-schema/src/de/verify.rs
@@ -170,7 +170,7 @@ impl<'de> de::Visitor<'de> for Visitor {
 
     fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
         match self.0 {
-            Schema::Integer(schema) => match schema.check_constraints(v as isize) {
+            Schema::Integer(schema) => match schema.check_constraints(v) {
                 Ok(()) => Ok(Verifier),
                 Err(err) => Err(E::custom(err)),
             },
@@ -180,10 +180,13 @@ impl<'de> de::Visitor<'de> for Visitor {
 
     fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
         match self.0 {
-            Schema::Integer(schema) => match schema.check_constraints(v as isize) {
-                Ok(()) => Ok(Verifier),
-                Err(err) => Err(E::custom(err)),
-            },
+            Schema::Integer(schema) => {
+                let val = v.try_into().or_else(|err| Err(E::custom(err)))?;
+                match schema.check_constraints(val) {
+                    Ok(()) => Ok(Verifier),
+                    Err(err) => Err(E::custom(err)),
+                }
+            }
             _ => Err(E::invalid_type(Unexpected::Unsigned(v), &self)),
         }
     }
diff --git a/proxmox-schema/src/schema.rs b/proxmox-schema/src/schema.rs
index 1b26c45b..e4ad971b 100644
--- a/proxmox-schema/src/schema.rs
+++ b/proxmox-schema/src/schema.rs
@@ -228,11 +228,11 @@ impl BooleanSchema {
 pub struct IntegerSchema {
     pub description: &'static str,
     /// Optional minimum.
-    pub minimum: Option<isize>,
+    pub minimum: Option<i64>,
     /// Optional maximum.
-    pub maximum: Option<isize>,
+    pub maximum: Option<i64>,
     /// Optional default.
-    pub default: Option<isize>,
+    pub default: Option<i64>,
 }
 
 impl IntegerSchema {
@@ -250,17 +250,17 @@ impl IntegerSchema {
         self
     }
 
-    pub const fn default(mut self, default: isize) -> Self {
+    pub const fn default(mut self, default: i64) -> Self {
         self.default = Some(default);
         self
     }
 
-    pub const fn minimum(mut self, minimum: isize) -> Self {
+    pub const fn minimum(mut self, minimum: i64) -> Self {
         self.minimum = Some(minimum);
         self
     }
 
-    pub const fn maximum(mut self, maximum: isize) -> Self {
+    pub const fn maximum(mut self, maximum: i64) -> Self {
         self.maximum = Some(maximum);
         self
     }
@@ -269,7 +269,7 @@ impl IntegerSchema {
         Schema::Integer(self)
     }
 
-    pub fn check_constraints(&self, value: isize) -> Result<(), Error> {
+    pub fn check_constraints(&self, value: i64) -> Result<(), Error> {
         if let Some(minimum) = self.minimum {
             if value < minimum {
                 bail!(
@@ -296,7 +296,7 @@ impl IntegerSchema {
     /// Verify JSON value using an `IntegerSchema`.
     pub fn verify_json(&self, data: &Value) -> Result<(), Error> {
         if let Some(value) = data.as_i64() {
-            self.check_constraints(value as isize)
+            self.check_constraints(value)
         } else {
             bail!("Expected integer value.");
         }
@@ -1406,7 +1406,7 @@ impl Schema {
                 Value::Bool(res)
             }
             Schema::Integer(integer_schema) => {
-                let res: isize = value_str.parse()?;
+                let res: i64 = value_str.parse()?;
                 integer_schema.check_constraints(res)?;
                 Value::Number(res.into())
             }
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH proxmox v4 2/2] pbs-api-types: fix values for integer schemas
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox v4 1/2] schema: use i64 for minimum / maximum / default integer values Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04 10:03   ` [pdm-devel] applied: " Wolfgang Bumiller
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-backup v4 1/1] api: change integer schema parameters to i64 Stefan Hanreich
                   ` (30 subsequent siblings)
  32 siblings, 1 reply; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

Because of the change from isize to i64 some casts have to be adjusted
so the types for the maximum / default values are correct again.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 pbs-api-types/src/datastore.rs | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/pbs-api-types/src/datastore.rs b/pbs-api-types/src/datastore.rs
index ee94ccad..fe73cbc4 100644
--- a/pbs-api-types/src/datastore.rs
+++ b/pbs-api-types/src/datastore.rs
@@ -93,14 +93,14 @@ pub const BACKUP_NAMESPACE_SCHEMA: Schema = StringSchema::new("Namespace.")
 pub const NS_MAX_DEPTH_SCHEMA: Schema =
     IntegerSchema::new("How many levels of namespaces should be operated on (0 == no recursion)")
         .minimum(0)
-        .maximum(MAX_NAMESPACE_DEPTH as isize)
-        .default(MAX_NAMESPACE_DEPTH as isize)
+        .maximum(MAX_NAMESPACE_DEPTH as i64)
+        .default(MAX_NAMESPACE_DEPTH as i64)
         .schema();
 
 pub const NS_MAX_DEPTH_REDUCED_SCHEMA: Schema =
 IntegerSchema::new("How many levels of namespaces should be operated on (0 == no recursion, empty == automatic full recursion, namespace depths reduce maximum allowed value)")
     .minimum(0)
-    .maximum(MAX_NAMESPACE_DEPTH as isize)
+    .maximum(MAX_NAMESPACE_DEPTH as i64)
     .schema();
 
 pub const DATASTORE_SCHEMA: Schema = StringSchema::new("Datastore name.")
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH proxmox-backup v4 1/1] api: change integer schema parameters to i64
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox v4 1/2] schema: use i64 for minimum / maximum / default integer values Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox v4 2/2] pbs-api-types: fix values for integer schemas Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04 12:46   ` [pdm-devel] applied: " Wolfgang Bumiller
  2025-09-04  8:18 ` [pdm-devel] [PATCH pve-network v4 1/6] sdn: api: return null for rollback / lock endpoints Stefan Hanreich
                   ` (29 subsequent siblings)
  32 siblings, 1 reply; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

The type for the minimum / maximum / default values for integer
schemas has changed to i64 (from isize). Fix all call sites that are
converting to isize to convert to i64 instead.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 pbs-tape/src/bin/pmt.rs           |  6 +++---
 proxmox-backup-client/src/main.rs |  2 +-
 pxar-bin/src/main.rs              |  6 +++---
 src/api2/backup/upload_chunk.rs   | 15 ++++++---------
 4 files changed, 13 insertions(+), 16 deletions(-)

diff --git a/pbs-tape/src/bin/pmt.rs b/pbs-tape/src/bin/pmt.rs
index d70c61e9..77b61314 100644
--- a/pbs-tape/src/bin/pmt.rs
+++ b/pbs-tape/src/bin/pmt.rs
@@ -28,17 +28,17 @@ use pbs_tape::{
 
 pub const FILE_MARK_COUNT_SCHEMA: Schema = IntegerSchema::new("File mark count.")
     .minimum(1)
-    .maximum(i32::MAX as isize)
+    .maximum(i32::MAX as i64)
     .schema();
 
 pub const FILE_MARK_POSITION_SCHEMA: Schema = IntegerSchema::new("File mark position (0 is BOT).")
     .minimum(0)
-    .maximum(i32::MAX as isize)
+    .maximum(i32::MAX as i64)
     .schema();
 
 pub const RECORD_COUNT_SCHEMA: Schema = IntegerSchema::new("Record count.")
     .minimum(1)
-    .maximum(i32::MAX as isize)
+    .maximum(i32::MAX as i64)
     .schema();
 
 pub const DRIVE_OPTION_SCHEMA: Schema =
diff --git a/proxmox-backup-client/src/main.rs b/proxmox-backup-client/src/main.rs
index 3f6c5adb..7dc2c486 100644
--- a/proxmox-backup-client/src/main.rs
+++ b/proxmox-backup-client/src/main.rs
@@ -726,7 +726,7 @@ fn spawn_catalog_upload(
                 type: Integer,
                 description: "Max number of entries to hold in memory.",
                 optional: true,
-                default: pbs_client::pxar::ENCODER_MAX_ENTRIES as isize,
+                default: pbs_client::pxar::ENCODER_MAX_ENTRIES as i64,
             },
             "dry-run": {
                 type: Boolean,
diff --git a/pxar-bin/src/main.rs b/pxar-bin/src/main.rs
index cccbb331..cba5646b 100644
--- a/pxar-bin/src/main.rs
+++ b/pxar-bin/src/main.rs
@@ -330,9 +330,9 @@ fn extract_archive(
             "entries-max": {
                 description: "Max number of entries loaded at once into memory",
                 optional: true,
-                default: ENCODER_MAX_ENTRIES as isize,
+                default: ENCODER_MAX_ENTRIES as i64,
                 minimum: 0,
-                maximum: isize::MAX,
+                maximum: i64::MAX,
             },
             "payload-output": {
                 description: "'ppxar' payload output data file to create split archive.",
@@ -354,7 +354,7 @@ async fn create_archive(
     no_fifos: bool,
     no_sockets: bool,
     exclude: Option<Vec<String>>,
-    entries_max: isize,
+    entries_max: i64,
     payload_output: Option<String>,
 ) -> Result<(), Error> {
     let patterns = {
diff --git a/src/api2/backup/upload_chunk.rs b/src/api2/backup/upload_chunk.rs
index f8b7ecbd..8dd7e4d5 100644
--- a/src/api2/backup/upload_chunk.rs
+++ b/src/api2/backup/upload_chunk.rs
@@ -134,10 +134,9 @@ pub const API_METHOD_UPLOAD_FIXED_CHUNK: ApiMethod = ApiMethod::new(
                 "encoded-size",
                 false,
                 &IntegerSchema::new("Encoded chunk size.")
-                    .minimum((std::mem::size_of::<DataBlobHeader>() as isize) + 1)
+                    .minimum((std::mem::size_of::<DataBlobHeader>() as i64) + 1)
                     .maximum(
-                        1024 * 1024 * 16
-                            + (std::mem::size_of::<EncryptedDataBlobHeader>() as isize)
+                        1024 * 1024 * 16 + (std::mem::size_of::<EncryptedDataBlobHeader>() as i64)
                     )
                     .schema()
             ),
@@ -197,10 +196,9 @@ pub const API_METHOD_UPLOAD_DYNAMIC_CHUNK: ApiMethod = ApiMethod::new(
                 "encoded-size",
                 false,
                 &IntegerSchema::new("Encoded chunk size.")
-                    .minimum((std::mem::size_of::<DataBlobHeader>() as isize) + 1)
+                    .minimum((std::mem::size_of::<DataBlobHeader>() as i64) + 1)
                     .maximum(
-                        1024 * 1024 * 16
-                            + (std::mem::size_of::<EncryptedDataBlobHeader>() as isize)
+                        1024 * 1024 * 16 + (std::mem::size_of::<EncryptedDataBlobHeader>() as i64)
                     )
                     .schema()
             ),
@@ -357,10 +355,9 @@ pub const API_METHOD_UPLOAD_BLOB: ApiMethod = ApiMethod::new(
                 "encoded-size",
                 false,
                 &IntegerSchema::new("Encoded blob size.")
-                    .minimum(std::mem::size_of::<DataBlobHeader>() as isize)
+                    .minimum(std::mem::size_of::<DataBlobHeader>() as i64)
                     .maximum(
-                        1024 * 1024 * 16
-                            + (std::mem::size_of::<EncryptedDataBlobHeader>() as isize)
+                        1024 * 1024 * 16 + (std::mem::size_of::<EncryptedDataBlobHeader>() as i64)
                     )
                     .schema()
             )
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH pve-network v4 1/6] sdn: api: return null for rollback / lock endpoints
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (2 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-backup v4 1/1] api: change integer schema parameters to i64 Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04 12:31   ` [pdm-devel] appled: " Wolfgang Bumiller
  2025-09-04  8:18 ` [pdm-devel] [PATCH pve-network v4 2/6] controllers: fix maximum value for ASN Stefan Hanreich
                   ` (28 subsequent siblings)
  32 siblings, 1 reply; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

lock_sdn_config can return a boolean value, which will then in turn
get returned as data from the API calls. Since we hint type null here,
this leads to problems with the pve-api-client in rust. Fix the return
value for this API call by adding an explicit return statement.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 src/PVE/API2/Network/SDN.pm | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/PVE/API2/Network/SDN.pm b/src/PVE/API2/Network/SDN.pm
index af00b1a..88b229c 100644
--- a/src/PVE/API2/Network/SDN.pm
+++ b/src/PVE/API2/Network/SDN.pm
@@ -192,6 +192,8 @@ __PACKAGE__->register_method({
                 $param->{'lock-token'},
             );
         }
+
+        return;
     },
 });
 
@@ -247,6 +249,8 @@ __PACKAGE__->register_method({
         PVE::Network::SDN::lock_sdn_config(
             $rollback, "could not rollback SDN configuration", $lock_token,
         );
+
+        return;
     },
 });
 
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH pve-network v4 2/6] controllers: fix maximum value for ASN
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (3 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH pve-network v4 1/6] sdn: api: return null for rollback / lock endpoints Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH pve-network v4 3/6] api: add state standard option Stefan Hanreich
                   ` (27 subsequent siblings)
  32 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

ASNs are 32-bit unsigned integers where the maximum value is
4294967295.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
index 021673b..e53000a 100644
--- a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
@@ -25,7 +25,7 @@ sub properties {
             type => 'integer',
             description => "autonomous system number",
             minimum => 0,
-            maximum => 4294967296,
+            maximum => 2**32 - 1,
         },
         fabric => {
             description => "SDN fabric to use as underlay for this EVPN controller.",
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH pve-network v4 3/6] api: add state standard option
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (4 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH pve-network v4 2/6] controllers: fix maximum value for ASN Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH pve-network v4 4/6] api: controllers: update schema of endpoints Stefan Hanreich
                   ` (26 subsequent siblings)
  32 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

GET calls on SDN entities with pending=1 show the state of the entity.
Define a common option for this, since it is the same across all SDN
entities. This will be used for improving the API documentation later
on.

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

diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index 83f2cc7..f2ecd4a 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -60,6 +60,16 @@ PVE::JSONSchema::register_standard_option(
     },
 );
 
+PVE::JSONSchema::register_standard_option(
+    'pve-sdn-config-state',
+    {
+        type => 'string',
+        enum => ['new', 'changed', 'deleted'],
+        description => 'State of the SDN configuration object.',
+        optional => 1,
+    },
+);
+
 # improve me : move status code inside plugins ?
 
 sub ifquery_check {
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH pve-network v4 4/6] api: controllers: update schema of endpoints
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (5 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH pve-network v4 3/6] api: add state standard option Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH pve-network v4 5/6] api: vnets: " Stefan Hanreich
                   ` (25 subsequent siblings)
  32 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

The possible properties returned by the controller endpoints were only
partly documented. Add all missing properties and update descriptions
for existing properties.

Update the descriptions of the schemas in the plugin to provide more
detailed information about the different configuration options.

Move all duplicate properties between the GET endpoints into its own
variable, so we can reuse them.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 src/PVE/API2/Network/SDN/Controllers.pm       | 116 +++++++++++++++++-
 src/PVE/Network/SDN/Controllers/BgpPlugin.pm  |   7 +-
 src/PVE/Network/SDN/Controllers/IsisPlugin.pm |   6 +-
 3 files changed, 119 insertions(+), 10 deletions(-)

diff --git a/src/PVE/API2/Network/SDN/Controllers.pm b/src/PVE/API2/Network/SDN/Controllers.pm
index 5c2b6c3..bc3ec09 100644
--- a/src/PVE/API2/Network/SDN/Controllers.pm
+++ b/src/PVE/API2/Network/SDN/Controllers.pm
@@ -34,6 +34,67 @@ my $api_sdn_controllers_config = sub {
     return $scfg;
 };
 
+my $CONTROLLER_PROPERTIES = {
+    asn => {
+        type => 'integer',
+        description => 'The local ASN of the controller. BGP & EVPN only.',
+        optional => 1,
+        minimum => 0,
+        maximum => 4294967295,
+    },
+    node => {
+        type => 'string',
+        optional => 1,
+        description => 'Node(s) where this controller is active.',
+    },
+    peers => {
+        type => 'string',
+        optional => 1,
+        description => 'Comma-separated list of the peers IP addresses.',
+    },
+    'bgp-multipath-as-relax' => {
+        type => 'boolean',
+        optional => 1,
+        description =>
+            'Consider different AS paths of equal length for multipath computation. BGP only.',
+    },
+    ebgp => {
+        type => 'boolean',
+        optional => 1,
+        description => "Enable eBGP (remote-as external). BGP only.",
+    },
+    'ebgp-multihop' => {
+        type => 'integer',
+        optional => 1,
+        description =>
+            "Set maximum amount of hops for eBGP peers. Needs ebgp set to 1. BGP only.",
+    },
+    loopback => {
+        description =>
+            "Name of the loopback/dummy interface that provides the Router-IP. BGP only.",
+        optional => 1,
+        type => 'string',
+    },
+    'isis-domain' => {
+        description => "Name of the IS-IS domain. IS-IS only.",
+        optional => 1,
+        type => 'string',
+    },
+    'isis-ifaces' => {
+        description =>
+            "Comma-separated list of interfaces where IS-IS should be active. IS-IS only.",
+        optional => 1,
+        type => 'string',
+        format => 'pve-iface-list',
+    },
+    'isis-net' => {
+        description => "Network Entity title for this node in the IS-IS network. IS-IS only.",
+        optional => 1,
+        type => 'string',
+        format => 'pve-sdn-isis-net',
+    },
+};
+
 __PACKAGE__->register_method({
     name => 'index',
     path => '',
@@ -70,10 +131,29 @@ __PACKAGE__->register_method({
         items => {
             type => "object",
             properties => {
-                controller => { type => 'string' },
-                type => { type => 'string' },
-                state => { type => 'string', optional => 1 },
-                pending => { type => 'boolean', optional => 1 },
+                digest => {
+                    type => 'string',
+                    description => 'Digest of the controller section.',
+                    optional => 1,
+                },
+                state => get_standard_option('pve-sdn-config-state'),
+                controller => {
+                    type => 'string',
+                    description => 'Name of the controller.',
+                },
+                type => {
+                    type => 'string',
+                    description => 'Type of the controller',
+                    enum => PVE::Network::SDN::Controllers::Plugin->lookup_types(),
+                },
+                pending => {
+                    type => 'object',
+                    description =>
+                        'Changes that have not yet been applied to the running configuration.',
+                    optional => 1,
+                    properties => $CONTROLLER_PROPERTIES,
+                },
+                %$CONTROLLER_PROPERTIES,
             },
         },
         links => [{ rel => 'child', href => "{controller}" }],
@@ -139,7 +219,33 @@ __PACKAGE__->register_method({
             },
         },
     },
-    returns => { type => 'object' },
+    returns => {
+        properties => {
+            digest => {
+                type => 'string',
+                description => 'Digest of the controller section.',
+                optional => 1,
+            },
+            state => get_standard_option('pve-sdn-config-state'),
+            controller => {
+                type => 'string',
+                description => 'Name of the controller.',
+            },
+            type => {
+                type => 'string',
+                description => 'Type of the controller',
+                enum => PVE::Network::SDN::Controllers::Plugin->lookup_types(),
+            },
+            pending => {
+                type => 'object',
+                description =>
+                    'Changes that have not yet been applied to the running configuration.',
+                optional => 1,
+                properties => $CONTROLLER_PROPERTIES,
+            },
+            %$CONTROLLER_PROPERTIES,
+        },
+    },
     code => sub {
         my ($param) = @_;
 
diff --git a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
index 5f3fcb0..c84b384 100644
--- a/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/BgpPlugin.pm
@@ -22,18 +22,21 @@ sub properties {
         'bgp-multipath-as-path-relax' => {
             type => 'boolean',
             optional => 1,
+            description =>
+                'Consider different AS paths of equal length for multipath computation.',
         },
         ebgp => {
             type => 'boolean',
             optional => 1,
-            description => "Enable ebgp. (remote-as external)",
+            description => "Enable eBGP (remote-as external).",
         },
         'ebgp-multihop' => {
             type => 'integer',
             optional => 1,
+            description => 'Set maximum amount of hops for eBGP peers.',
         },
         loopback => {
-            description => "source loopback interface.",
+            description => "Name of the loopback/dummy interface that provides the Router-IP.",
             type => 'string',
         },
         node => get_standard_option('pve-node'),
diff --git a/src/PVE/Network/SDN/Controllers/IsisPlugin.pm b/src/PVE/Network/SDN/Controllers/IsisPlugin.pm
index 716bb0f..3a9acfd 100644
--- a/src/PVE/Network/SDN/Controllers/IsisPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/IsisPlugin.pm
@@ -31,16 +31,16 @@ sub pve_verify_sdn_isis_net {
 sub properties {
     return {
         'isis-domain' => {
-            description => "ISIS domain.",
+            description => "Name of the IS-IS domain.",
             type => 'string',
         },
         'isis-ifaces' => {
-            description => "ISIS interface.",
+            description => "Comma-separated list of interfaces where IS-IS should be active.",
             type => 'string',
             format => 'pve-iface-list',
         },
         'isis-net' => {
-            description => "ISIS network entity title.",
+            description => "Network Entity title for this node in the IS-IS network.",
             type => 'string',
             format => 'pve-sdn-isis-net',
         },
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH pve-network v4 5/6] api: vnets: update schema of endpoints
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (6 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH pve-network v4 4/6] api: controllers: update schema of endpoints Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH pve-network v4 6/6] api: zones: " Stefan Hanreich
                   ` (24 subsequent siblings)
  32 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

The possible properties returned by the vnet endpoints were only
partly documented. Add all missing properties and improve descriptions
for existing properties.

Extract all duplicate properties into a separate variable, so we
don't have to rewrite the whole API definition for every endpoint.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 src/PVE/API2/Network/SDN/Vnets.pm | 92 ++++++++++++++++++++++++++++++-
 src/PVE/Network/SDN/VnetPlugin.pm | 21 +++++--
 2 files changed, 105 insertions(+), 8 deletions(-)

diff --git a/src/PVE/API2/Network/SDN/Vnets.pm b/src/PVE/API2/Network/SDN/Vnets.pm
index e6eb5d4..1d9e500 100644
--- a/src/PVE/API2/Network/SDN/Vnets.pm
+++ b/src/PVE/API2/Network/SDN/Vnets.pm
@@ -74,6 +74,40 @@ my $check_vnet_access = sub {
     $rpcenv->check_any($authuser, "/sdn/zones/$zoneid/$vnet", $privs);
 };
 
+my $VNET_PROPERTIES = {
+    alias => {
+        type => 'string',
+        description => "Alias name of the VNet.",
+        pattern => qr/[\(\)-_.\w\d\s]{0,256}/i,
+        maxLength => 256,
+        optional => 1,
+    },
+    'isolate-ports' => {
+        type => 'boolean',
+        description =>
+            "If true, sets the isolated property for all interfaces on the bridge of this VNet.",
+        optional => 1,
+    },
+    tag => {
+        type => 'integer',
+        description =>
+            'VLAN Tag (for VLAN or QinQ zones) or VXLAN VNI (for VXLAN or EVPN zones).',
+        optional => 1,
+        minimum => 1,
+        maximum => 16777215,
+    },
+    vlanaware => {
+        type => 'boolean',
+        description => 'Allow VLANs to pass through this VNet.',
+        optional => 1,
+    },
+    zone => {
+        type => 'string',
+        description => 'Name of the zone this VNet belongs to.',
+        optional => 1,
+    },
+};
+
 __PACKAGE__->register_method({
     name => 'index',
     path => '',
@@ -103,7 +137,33 @@ __PACKAGE__->register_method({
         type => 'array',
         items => {
             type => "object",
-            properties => {},
+            properties => {
+                digest => {
+                    type => 'string',
+                    optional => 1,
+                    description => 'Digest of the VNet section.',
+                },
+                state => get_standard_option('pve-sdn-config-state'),
+                type => {
+                    type => 'string',
+                    enum => ['vnet'],
+                    optional => 0,
+                    description => 'Type of the VNet.',
+                },
+                vnet => {
+                    type => 'string',
+                    optional => 0,
+                    description => 'Name of the VNet.',
+                },
+                pending => {
+                    type => 'object',
+                    description =>
+                        'Changes that have not yet been applied to the running configuration.',
+                    optional => 1,
+                    properties => $VNET_PROPERTIES,
+                },
+                %$VNET_PROPERTIES,
+            },
         },
         links => [{ rel => 'child', href => "{vnet}" }],
     },
@@ -171,7 +231,35 @@ __PACKAGE__->register_method({
             },
         },
     },
-    returns => { type => 'object' },
+    returns => {
+        properties => {
+            digest => {
+                type => 'string',
+                optional => 1,
+                description => 'Digest of the VNet section.',
+            },
+            state => get_standard_option('pve-sdn-config-state'),
+            type => {
+                type => 'string',
+                enum => ['vnet'],
+                optional => 0,
+                description => 'Type of the VNet.',
+            },
+            vnet => {
+                type => 'string',
+                optional => 0,
+                description => 'Name of the VNet.',
+            },
+            pending => {
+                type => 'object',
+                description =>
+                    'Changes that have not yet been applied to the running configuration.',
+                optional => 1,
+                properties => $VNET_PROPERTIES,
+            },
+            %$VNET_PROPERTIES,
+        },
+    },
     code => sub {
         my ($param) = @_;
 
diff --git a/src/PVE/Network/SDN/VnetPlugin.pm b/src/PVE/Network/SDN/VnetPlugin.pm
index 035aaca..717438c 100644
--- a/src/PVE/Network/SDN/VnetPlugin.pm
+++ b/src/PVE/Network/SDN/VnetPlugin.pm
@@ -60,30 +60,39 @@ sub properties {
     return {
         zone => {
             type => 'string',
-            description => "zone id",
+            description => 'Name of the zone this VNet belongs to.',
         },
         type => {
-            description => "Type",
+            type => 'string',
+            enum => ['vnet'],
+            description => 'Type of the VNet.',
             optional => 1,
         },
         tag => {
             type => 'integer',
-            description => "vlan or vxlan id",
+            description =>
+                'VLAN Tag (for VLAN or QinQ zones) or VXLAN VNI (for VXLAN or EVPN zones).',
+            optional => 1,
+            minimum => 1,
+            maximum => 16777215,
         },
         vlanaware => {
             type => 'boolean',
-            description => 'Allow vm VLANs to pass through this vnet.',
+            description => 'Allow VLANs to pass through this vnet.',
+            optional => 1,
         },
         alias => {
             type => 'string',
-            description => "alias name of the vnet",
+            description => "Alias name of the VNet.",
             pattern => qr/[\(\)-_.\w\d\s]{0,256}/i,
             maxLength => 256,
             optional => 1,
         },
         'isolate-ports' => {
             type => 'boolean',
-            description => "If true, sets the isolated property for all members of this VNet",
+            description =>
+                "If true, sets the isolated property for all interfaces on the bridge of this VNet.",
+            optional => 1,
         },
     };
 }
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH pve-network v4 6/6] api: zones: update schema of endpoints
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (7 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH pve-network v4 5/6] api: vnets: " Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-api-types v4 1/6] sdn: add list/create zone endpoints Stefan Hanreich
                   ` (23 subsequent siblings)
  32 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

The possible properties returned by the zone endpoints were only
partly documented. Add all missing properties and improve descriptions
for existing properties.

Extract all duplicate properties into a separate variable, so we
don't have to rewrite the whole API definition for every endpoint.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 src/PVE/API2/Network/SDN/Zones.pm        | 204 +++++++++++++++++++++--
 src/PVE/Network/SDN/Zones/EvpnPlugin.pm  |  22 ++-
 src/PVE/Network/SDN/Zones/QinQPlugin.pm  |   6 +-
 src/PVE/Network/SDN/Zones/VlanPlugin.pm  |   1 +
 src/PVE/Network/SDN/Zones/VxlanPlugin.pm |  16 +-
 5 files changed, 220 insertions(+), 29 deletions(-)

diff --git a/src/PVE/API2/Network/SDN/Zones.pm b/src/PVE/API2/Network/SDN/Zones.pm
index 0e4726b..8d829a9 100644
--- a/src/PVE/API2/Network/SDN/Zones.pm
+++ b/src/PVE/API2/Network/SDN/Zones.pm
@@ -62,6 +62,148 @@ my $api_sdn_zones_config = sub {
     return $scfg;
 };
 
+my $ZONE_PROPERTIES = {
+    mtu => {
+        type => 'integer',
+        optional => 1,
+        description => 'MTU of the zone, will be used for the created VNet bridges.',
+    },
+    dns => {
+        type => 'string',
+        optional => 1,
+        description => 'ID of the DNS server for this zone.',
+    },
+    reversedns => {
+        type => 'string',
+        optional => 1,
+        description => 'ID of the reverse DNS server for this zone.',
+    },
+    dnszone => {
+        type => 'string',
+        optional => 1,
+        description => 'Domain name for this zone.',
+    },
+    ipam => {
+        type => 'string',
+        optional => 1,
+        description => 'ID of the IPAM for this zone.',
+    },
+    dhcp => {
+        type => 'string',
+        enum => ['dnsmasq'],
+        optional => 1,
+        description => 'Name of DHCP server backend for this zone.',
+    },
+    'rt-import' => {
+        type => 'string',
+        optional => 1,
+        description =>
+            'Comma-separated list of Route Targets that should be imported into the VRF of the zone. EVPN zone only.',
+        format => 'pve-sdn-bgp-rt-list',
+    },
+    'vrf-vxlan' => {
+        type => 'integer',
+        optional => 1,
+        description => 'VNI for the zone VRF. EVPN zone only.',
+        minimum => 1,
+        maximum => 16777215,
+    },
+    mac => {
+        type => 'string',
+        optional => 1,
+        description => 'MAC address of the anycast router for this zone.',
+    },
+    controller => {
+        type => 'string',
+        optional => 1,
+        description => 'ID of the controller for this zone. EVPN zone only.',
+    },
+    nodes => {
+        type => 'string',
+        optional => 1,
+        description => 'Nodes where this zone should be created.',
+    },
+    'exitnodes' => get_standard_option(
+        'pve-node-list',
+        {
+            description =>
+                "List of PVE Nodes that should act as exit node for this zone. EVPN zone only.",
+            optional => 1,
+        },
+    ),
+    'exitnodes-local-routing' => {
+        type => 'boolean',
+        description =>
+            "Create routes on the exit nodes, so they can connect to EVPN guests. EVPN zone only.",
+        optional => 1,
+    },
+    'exitnodes-primary' => get_standard_option(
+        'pve-node',
+        {
+            description => "Force traffic through this exitnode first. EVPN zone only.",
+            optional => 1,
+        },
+    ),
+    'advertise-subnets' => {
+        type => 'boolean',
+        description =>
+            "Advertise IP prefixes (Type-5 routes) instead of MAC/IP pairs (Type-2 routes). EVPN zone only.",
+        optional => 1,
+    },
+    'disable-arp-nd-suppression' => {
+        type => 'boolean',
+        description =>
+            "Suppress IPv4 ARP && IPv6 Neighbour Discovery messages. EVPN zone only.",
+        optional => 1,
+    },
+    'rt-import' => {
+        type => 'string',
+        description =>
+            "Route-Targets that should be imported into the VRF of this zone via BGP. EVPN zone only.",
+        optional => 1,
+        format => 'pve-sdn-bgp-rt-list',
+    },
+    tag => {
+        type => 'integer',
+        minimum => 0,
+        optional => 1,
+        description => "Service-VLAN Tag (outer VLAN). QinQ zone only",
+    },
+    'vlan-protocol' => {
+        type => 'string',
+        enum => ['802.1q', '802.1ad'],
+        default => '802.1q',
+        optional => 1,
+        description => "VLAN protocol for the creation of the QinQ zone. QinQ zone only.",
+    },
+    'peers' => {
+        description =>
+            "Comma-separated list of peers, that are part of the VXLAN zone. Usually the IPs of the nodes. VXLAN zone only.",
+        type => 'string',
+        format => 'ip-list',
+        optional => 1,
+    },
+    'vxlan-port' => {
+        description =>
+            "UDP port that should be used for the VXLAN tunnel (default 4789). VXLAN zone only.",
+        minimum => 1,
+        maximum => 65536,
+        type => 'integer',
+        optional => 1,
+        default => 4789,
+    },
+    'bridge' => {
+        type => 'string',
+        description => 'the bridge for which VLANs should be managed. VLAN & QinQ zone only.',
+        optional => 1,
+    },
+    'bridge-disable-mac-learning' => {
+        type => 'boolean',
+        description => "Disable auto mac learning. VLAN zone only.",
+        optional => 1,
+    },
+};
+
 __PACKAGE__->register_method({
     name => 'index',
     path => '',
@@ -98,17 +240,29 @@ __PACKAGE__->register_method({
         items => {
             type => "object",
             properties => {
-                zone => { type => 'string' },
-                type => { type => 'string' },
-                mtu => { type => 'integer', optional => 1 },
-                dns => { type => 'string', optional => 1 },
-                reversedns => { type => 'string', optional => 1 },
-                dnszone => { type => 'string', optional => 1 },
-                ipam => { type => 'string', optional => 1 },
-                dhcp => { type => 'string', optional => 1 },
-                pending => { type => 'boolean', optional => 1 },
-                state => { type => 'string', optional => 1 },
-                nodes => { type => 'string', optional => 1 },
+                digest => {
+                    type => 'string',
+                    description => 'Digest of the controller section.',
+                    optional => 1,
+                },
+                state => get_standard_option('pve-sdn-config-state'),
+                zone => {
+                    type => 'string',
+                    description => 'Name of the zone.',
+                },
+                type => {
+                    type => 'string',
+                    description => 'Type of the zone.',
+                    enum => PVE::Network::SDN::Zones::Plugin->lookup_types(),
+                },
+                pending => {
+                    type => 'object',
+                    description =>
+                        'Changes that have not yet been applied to the running configuration.',
+                    optional => 1,
+                    properties => $ZONE_PROPERTIES,
+                },
+                %$ZONE_PROPERTIES,
             },
         },
         links => [{ rel => 'child', href => "{zone}" }],
@@ -174,7 +328,33 @@ __PACKAGE__->register_method({
             },
         },
     },
-    returns => { type => 'object' },
+    returns => {
+        properties => {
+            digest => {
+                type => 'string',
+                description => 'Digest of the controller section.',
+                optional => 1,
+            },
+            state => get_standard_option('pve-sdn-config-state'),
+            zone => {
+                type => 'string',
+                description => 'Name of the zone.',
+            },
+            type => {
+                type => 'string',
+                description => 'Type of the zone.',
+                enum => PVE::Network::SDN::Zones::Plugin->lookup_types(),
+            },
+            pending => {
+                type => 'object',
+                description =>
+                    'Changes that have not yet been applied to the running configuration.',
+                optional => 1,
+                properties => $ZONE_PROPERTIES,
+            },
+            %$ZONE_PROPERTIES,
+        },
+    },
     code => sub {
         my ($param) = @_;
 
diff --git a/src/PVE/Network/SDN/Zones/EvpnPlugin.pm b/src/PVE/Network/SDN/Zones/EvpnPlugin.pm
index 0153364..6d89499 100644
--- a/src/PVE/Network/SDN/Zones/EvpnPlugin.pm
+++ b/src/PVE/Network/SDN/Zones/EvpnPlugin.pm
@@ -45,41 +45,47 @@ sub properties {
     return {
         'vrf-vxlan' => {
             type => 'integer',
-            description => "l3vni.",
+            description => "VNI for the zone VRF.",
+            minimum => 1,
+            maximum => 16777215,
         },
         'controller' => {
             type => 'string',
-            description => "Frr router name",
+            description => 'Controller for this zone.',
         },
         'mac' => {
             type => 'string',
-            description => "Anycast logical router mac address",
+            description => "Anycast logical router mac address.",
             optional => 1,
             format => 'mac-addr',
         },
         'exitnodes' => get_standard_option('pve-node-list'),
         'exitnodes-local-routing' => {
             type => 'boolean',
-            description => "Allow exitnodes to connect to evpn guests",
+            description => "Allow exitnodes to connect to EVPN guests.",
             optional => 1,
         },
         'exitnodes-primary' => get_standard_option(
             'pve-node',
-            { description => "Force traffic to this exitnode first." },
+            {
+                description => "Force traffic through this exitnode first.",
+            },
         ),
         'advertise-subnets' => {
             type => 'boolean',
-            description => "Advertise evpn subnets if you have silent hosts",
+            description =>
+                "Advertise IP prefixes (Type-5 routes) instead of MAC/IP pairs (Type-2 routes).",
             optional => 1,
         },
         'disable-arp-nd-suppression' => {
             type => 'boolean',
-            description => "Disable ipv4 arp && ipv6 neighbour discovery suppression",
+            description => "Suppress IPv4 ARP && IPv6 Neighbour Discovery messages.",
             optional => 1,
         },
         'rt-import' => {
             type => 'string',
-            description => "Route-Target import",
+            description =>
+                'List of Route Targets that should be imported into the VRF of the zone.',
             optional => 1,
             format => 'pve-sdn-bgp-rt-list',
         },
diff --git a/src/PVE/Network/SDN/Zones/QinQPlugin.pm b/src/PVE/Network/SDN/Zones/QinQPlugin.pm
index 5806e69..a75940c 100644
--- a/src/PVE/Network/SDN/Zones/QinQPlugin.pm
+++ b/src/PVE/Network/SDN/Zones/QinQPlugin.pm
@@ -18,11 +18,11 @@ sub properties {
         tag => {
             type => 'integer',
             minimum => 0,
-            description => "Service-VLAN Tag",
+            description => "Service-VLAN Tag (outer VLAN)",
         },
         mtu => {
             type => 'integer',
-            description => "MTU",
+            description => "MTU of the zone, will be used for the created VNet bridges.",
             optional => 1,
         },
         'vlan-protocol' => {
@@ -30,6 +30,8 @@ sub properties {
             enum => ['802.1q', '802.1ad'],
             default => '802.1q',
             optional => 1,
+            description =>
+                "Which VLAN protocol should be used for the creation of the QinQ zone.",
         },
     };
 }
diff --git a/src/PVE/Network/SDN/Zones/VlanPlugin.pm b/src/PVE/Network/SDN/Zones/VlanPlugin.pm
index 90f16bf..9102b34 100644
--- a/src/PVE/Network/SDN/Zones/VlanPlugin.pm
+++ b/src/PVE/Network/SDN/Zones/VlanPlugin.pm
@@ -27,6 +27,7 @@ sub properties {
     return {
         'bridge' => {
             type => 'string',
+            description => 'The bridge for which VLANs should be managed.',
         },
         'bridge-disable-mac-learning' => {
             type => 'boolean',
diff --git a/src/PVE/Network/SDN/Zones/VxlanPlugin.pm b/src/PVE/Network/SDN/Zones/VxlanPlugin.pm
index 8f6fba0..1db610f 100644
--- a/src/PVE/Network/SDN/Zones/VxlanPlugin.pm
+++ b/src/PVE/Network/SDN/Zones/VxlanPlugin.pm
@@ -27,20 +27,22 @@ sub type {
 sub properties {
     return {
         'peers' => {
-            description => "peers address list.",
+            description =>
+                "Comma-separated list of peers, that are part of the VXLAN zone. Usually the IPs of the nodes.",
             type => 'string',
             format => 'ip-list',
         },
-        fabric => {
-            description => "SDN fabric to use as underlay for this VXLAN zone.",
-            type => 'string',
-            format => 'pve-sdn-fabric-id',
-        },
         'vxlan-port' => {
-            description => "Vxlan tunnel udp port (default 4789).",
+            description => "UDP port that should be used for the VXLAN tunnel (default 4789).",
             minimum => 1,
             maximum => 65536,
             type => 'integer',
+            default => 4789,
+        },
+        fabric => {
+            description => "SDN fabric to use as underlay for this VXLAN zone.",
+            type => 'string',
+            format => 'pve-sdn-fabric-id',
         },
     };
 }
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH proxmox-api-types v4 1/6] sdn: add list/create zone endpoints
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (8 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH pve-network v4 6/6] api: zones: " Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04 12:42   ` [pdm-devel] applied: " Wolfgang Bumiller
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-api-types v4 2/6] sdn: add list/create vnet endpoints Stefan Hanreich
                   ` (22 subsequent siblings)
  32 siblings, 1 reply; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 pve-api-types/generate.pl            | 18 +++++++++
 pve-api-types/src/types/verifiers.rs | 56 ++++++++++++++++++++++++++++
 2 files changed, 74 insertions(+)

diff --git a/pve-api-types/generate.pl b/pve-api-types/generate.pl
index cd08f3d..dc45d98 100644
--- a/pve-api-types/generate.pl
+++ b/pve-api-types/generate.pl
@@ -79,6 +79,8 @@ Schema2Rust::register_format('bridge-pair' => { code => 'verifiers::verify_bridg
 
 Schema2Rust::register_format('pve-task-status-type' => { regex => '^(?i:ok|error|warning|unknown)$' });
 
+Schema2Rust::register_format('pve-sdn-zone-id' => { code => 'verifiers::verify_sdn_id' });
+
 Schema2Rust::register_enum_variant('PveVmCpuConfReportedModel::486' => 'I486');
 Schema2Rust::register_enum_variant('QemuConfigEfidisk0Efitype::2m' => 'Mb2');
 Schema2Rust::register_enum_variant('QemuConfigEfidisk0Efitype::4m' => 'Mb4');
@@ -104,6 +106,10 @@ Schema2Rust::register_format('pve-iface' => { regex => '^[a-zA-Z][a-zA-Z0-9_]{1,
 
 Schema2Rust::register_format('pve-vlan-id-or-range' => { code => 'verifiers::verify_vlan_id_or_range' });
 
+Schema2Rust::register_format('pve-sdn-bgp-rt' => { code => 'verifiers::verify_sdn_bgp_rt' });
+Schema2Rust::register_format('pve-sdn-controller-id' => { code => 'verifiers::verify_sdn_controller_id' });
+Schema2Rust::register_format('pve-sdn-isis-net' => { regex => '^[a-fA-F0-9]{2}(\.[a-fA-F0-9]{4}){3,9}\.[a-fA-F0-9]{2}$' });
+
 # This is used as both a task status and guest status.
 Schema2Rust::generate_enum('IsRunning', {
     type => 'string',
@@ -328,6 +334,18 @@ api(GET => '/nodes/{node}/apt/update', 'list_available_updates', 'return-name' =
 api(POST => '/nodes/{node}/apt/update', 'update_apt_database', 'output-type' => 'PveUpid', 'param-name' => 'AptUpdateParams');
 api(GET => '/nodes/{node}/apt/changelog', 'get_package_changelog', 'output-type' => 'String');
 
+Schema2Rust::generate_enum('SdnObjectState', {
+    type => 'string',
+    description => "The state of an SDN object.",
+    enum => ['new', 'deleted', 'changed'],
+});
+
+api(GET => '/cluster/sdn/zones', 'list_zones', 'return-name' => 'SdnZone');
+Schema2Rust::derive('SdnZone' => 'Clone', 'PartialEq');
+Schema2Rust::derive('SdnZonePending' => 'Clone', 'PartialEq');
+api(POST => '/cluster/sdn/zones', 'create_zone', 'param-name' => 'CreateZone');
+Schema2Rust::derive('CreateZone' => 'Clone', 'PartialEq');
+
 # NOW DUMP THE CODE:
 #
 # We generate one file for API types, and one for API method calls.
diff --git a/pve-api-types/src/types/verifiers.rs b/pve-api-types/src/types/verifiers.rs
index e0ad4ca..9a88118 100644
--- a/pve-api-types/src/types/verifiers.rs
+++ b/pve-api-types/src/types/verifiers.rs
@@ -29,6 +29,12 @@ pub IFACE_RE = r##"^(?i)[a-z][a-z0-9_]{1,20}([:\.]\d+)?$"##;
 
 pub VLAN_ID_OR_RANGE = r##"^(\d+)(?:-(\d+))?$"##;
 
+pub SDN_ID = r##"^(?i)[a-z][a-z0-9]+$"##;
+
+pub SDN_CONTROLLER_ID = r##"^(?i)[a-z][a-z0-9_-]*[a-z0-9]$"##;
+
+pub SDN_BGP_RT = r##"^(\d+):(\d+)$"##;
+
 }
 
 pub fn verify_volume_id(s: &str) -> Result<(), Error> {
@@ -228,3 +234,53 @@ pub fn verify_vlan_id_or_range(s: &str) -> Result<(), Error> {
 
     Ok(())
 }
+
+pub fn verify_sdn_id(s: &str) -> Result<(), Error> {
+    if s.len() > 8 {
+        bail!("SDN ID cannot be longer than 8 characters")
+    }
+
+    if !SDN_ID.is_match(s) {
+        bail!("SDN ID contains illegal characters");
+    }
+
+    Ok(())
+}
+
+pub fn verify_sdn_controller_id(s: &str) -> Result<(), Error> {
+    if s.len() > 64 {
+        bail!("SDN controller ID cannot be longer than 64 characters")
+    }
+
+    if !SDN_CONTROLLER_ID.is_match(s) {
+        bail!("SDN controller ID contains illegal characters");
+    }
+
+    Ok(())
+}
+
+pub fn verify_sdn_bgp_rt(s: &str) -> Result<(), Error> {
+    let captures = SDN_BGP_RT
+        .captures(s)
+        .ok_or_else(|| format_err!("invalid BGP RT: '{s}"))?;
+
+    match (captures.get(1), captures.get(2)) {
+        (Some(asn), Some(vni)) => {
+            asn.as_str()
+                .parse::<u32>()
+                .map_err(|_| format_err!("Invalid ASN in BGP RT: {s}"))?;
+
+            let vni: u32 = vni
+                .as_str()
+                .parse()
+                .map_err(|_| format_err!("Invalid VNI in BGP RT: {s}"))?;
+
+            if vni > 16_777_215 {
+                bail!("invalid VNI in BGP RT: '{s}'")
+            }
+        }
+        _ => bail!("invalid BGP RT: '{s}"),
+    }
+
+    Ok(())
+}
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH proxmox-api-types v4 2/6] sdn: add list/create vnet endpoints
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (9 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-api-types v4 1/6] sdn: add list/create zone endpoints Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-api-types v4 3/6] sdn: add list/create controller endpoints Stefan Hanreich
                   ` (21 subsequent siblings)
  32 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 pve-api-types/generate.pl | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/pve-api-types/generate.pl b/pve-api-types/generate.pl
index dc45d98..900cb9f 100644
--- a/pve-api-types/generate.pl
+++ b/pve-api-types/generate.pl
@@ -80,6 +80,7 @@ Schema2Rust::register_format('bridge-pair' => { code => 'verifiers::verify_bridg
 Schema2Rust::register_format('pve-task-status-type' => { regex => '^(?i:ok|error|warning|unknown)$' });
 
 Schema2Rust::register_format('pve-sdn-zone-id' => { code => 'verifiers::verify_sdn_id' });
+Schema2Rust::register_format('pve-sdn-vnet-id' => { code => 'verifiers::verify_sdn_id' });
 
 Schema2Rust::register_enum_variant('PveVmCpuConfReportedModel::486' => 'I486');
 Schema2Rust::register_enum_variant('QemuConfigEfidisk0Efitype::2m' => 'Mb2');
@@ -346,6 +347,12 @@ Schema2Rust::derive('SdnZonePending' => 'Clone', 'PartialEq');
 api(POST => '/cluster/sdn/zones', 'create_zone', 'param-name' => 'CreateZone');
 Schema2Rust::derive('CreateZone' => 'Clone', 'PartialEq');
 
+api(GET => '/cluster/sdn/vnets', 'list_vnets', 'return-name' => 'SdnVnet');
+Schema2Rust::derive('SdnVnet' => 'Clone', 'PartialEq');
+Schema2Rust::derive('SdnVnetPending' => 'Clone', 'PartialEq');
+api(POST => '/cluster/sdn/vnets', 'create_vnet', 'param-name' => 'CreateVnet');
+Schema2Rust::derive('CreateVnet' => 'Clone', 'PartialEq');
+
 # NOW DUMP THE CODE:
 #
 # We generate one file for API types, and one for API method calls.
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH proxmox-api-types v4 3/6] sdn: add list/create controller endpoints
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (10 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-api-types v4 2/6] sdn: add list/create vnet endpoints Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-api-types v4 4/6] sdn: add sdn configuration locking endpoints Stefan Hanreich
                   ` (20 subsequent siblings)
  32 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 pve-api-types/generate.pl | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/pve-api-types/generate.pl b/pve-api-types/generate.pl
index 900cb9f..796d111 100644
--- a/pve-api-types/generate.pl
+++ b/pve-api-types/generate.pl
@@ -110,6 +110,7 @@ Schema2Rust::register_format('pve-vlan-id-or-range' => { code => 'verifiers::ver
 Schema2Rust::register_format('pve-sdn-bgp-rt' => { code => 'verifiers::verify_sdn_bgp_rt' });
 Schema2Rust::register_format('pve-sdn-controller-id' => { code => 'verifiers::verify_sdn_controller_id' });
 Schema2Rust::register_format('pve-sdn-isis-net' => { regex => '^[a-fA-F0-9]{2}(\.[a-fA-F0-9]{4}){3,9}\.[a-fA-F0-9]{2}$' });
+Schema2Rust::register_format('pve-sdn-fabric-id' => { code => 'verifiers::verify_sdn_id' });
 
 # This is used as both a task status and guest status.
 Schema2Rust::generate_enum('IsRunning', {
@@ -347,6 +348,12 @@ Schema2Rust::derive('SdnZonePending' => 'Clone', 'PartialEq');
 api(POST => '/cluster/sdn/zones', 'create_zone', 'param-name' => 'CreateZone');
 Schema2Rust::derive('CreateZone' => 'Clone', 'PartialEq');
 
+api(GET => '/cluster/sdn/controllers', 'list_controllers', 'return-name' => 'SdnController');
+Schema2Rust::derive('SdnController' => 'Clone', 'PartialEq');
+Schema2Rust::derive('SdnControllerPending' => 'Clone', 'PartialEq');
+api(POST => '/cluster/sdn/controllers', 'create_controller', 'param-name' => 'CreateController');
+Schema2Rust::derive('CreateController' => 'Clone', 'PartialEq');
+
 api(GET => '/cluster/sdn/vnets', 'list_vnets', 'return-name' => 'SdnVnet');
 Schema2Rust::derive('SdnVnet' => 'Clone', 'PartialEq');
 Schema2Rust::derive('SdnVnetPending' => 'Clone', 'PartialEq');
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH proxmox-api-types v4 4/6] sdn: add sdn configuration locking endpoints
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (11 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-api-types v4 3/6] sdn: add list/create controller endpoints Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-api-types v4 5/6] tasks: add helper for querying successfully finished tasks Stefan Hanreich
                   ` (19 subsequent siblings)
  32 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 pve-api-types/generate.pl | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/pve-api-types/generate.pl b/pve-api-types/generate.pl
index 796d111..cf154c6 100644
--- a/pve-api-types/generate.pl
+++ b/pve-api-types/generate.pl
@@ -360,6 +360,12 @@ Schema2Rust::derive('SdnVnetPending' => 'Clone', 'PartialEq');
 api(POST => '/cluster/sdn/vnets', 'create_vnet', 'param-name' => 'CreateVnet');
 Schema2Rust::derive('CreateVnet' => 'Clone', 'PartialEq');
 
+api(POST => '/cluster/sdn/lock', 'acquire_sdn_lock', 'param-name' => 'CreateSdnLock', 'output-type' => 'String');
+api(DELETE => '/cluster/sdn/lock', 'release_sdn_lock', 'param-name' => 'ReleaseSdnLock');
+
+api(PUT => '/cluster/sdn', 'sdn_apply', 'param-name' => 'ReloadSdn', 'output-type' => 'PveUpid');
+api(POST => '/cluster/sdn/rollback', 'rollback_sdn_changes', 'param-name' => 'RollbackSdn');
+
 # NOW DUMP THE CODE:
 #
 # We generate one file for API types, and one for API method calls.
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH proxmox-api-types v4 5/6] tasks: add helper for querying successfully finished tasks
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (12 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-api-types v4 4/6] sdn: add sdn configuration locking endpoints Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-api-types v4 6/6] sdn: add helpers for pending values Stefan Hanreich
                   ` (18 subsequent siblings)
  32 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 pve-api-types/src/types/mod.rs | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/pve-api-types/src/types/mod.rs b/pve-api-types/src/types/mod.rs
index deb0938..3fea258 100644
--- a/pve-api-types/src/types/mod.rs
+++ b/pve-api-types/src/types/mod.rs
@@ -211,4 +211,8 @@ impl TaskStatus {
     pub fn is_running(&self) -> bool {
         self.status.is_running()
     }
+
+    pub fn finished_successfully(&self) -> Option<bool> {
+        self.exitstatus.as_ref().map(|status| status == "OK")
+    }
 }
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH proxmox-api-types v4 6/6] sdn: add helpers for pending values
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (13 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-api-types v4 5/6] tasks: add helper for querying successfully finished tasks Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 01/15] server: add locked sdn client helpers Stefan Hanreich
                   ` (17 subsequent siblings)
  32 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

The SDN API returns pending values in the pending field of the entity.
Add helpers that return the pending value if it exists, or the actual
value if there is no pending value for that field.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 pve-api-types/src/lib.rs |  1 +
 pve-api-types/src/sdn.rs | 33 +++++++++++++++++++++++++++++++++
 2 files changed, 34 insertions(+)
 create mode 100644 pve-api-types/src/sdn.rs

diff --git a/pve-api-types/src/lib.rs b/pve-api-types/src/lib.rs
index 709192d..b42a0c7 100644
--- a/pve-api-types/src/lib.rs
+++ b/pve-api-types/src/lib.rs
@@ -7,4 +7,5 @@ pub use types::*;
 #[cfg(feature = "client-util")]
 pub mod client;
 
+mod sdn;
 mod tags;
diff --git a/pve-api-types/src/sdn.rs b/pve-api-types/src/sdn.rs
new file mode 100644
index 0000000..000da6f
--- /dev/null
+++ b/pve-api-types/src/sdn.rs
@@ -0,0 +1,33 @@
+use crate::{SdnController, SdnVnet, SdnZone};
+
+impl SdnVnet {
+    /// returns the tag from the pending property if it has a value, otherwise it returns self.tag
+    pub fn tag_pending(&self) -> Option<u32> {
+        self.pending
+            .as_ref()
+            .and_then(|pending| pending.tag)
+            .or(self.tag)
+    }
+
+    /// returns the zone from the pending property if it has a value, otherwise it returns
+    /// self.zone
+    pub fn zone_pending(&self) -> String {
+        self.pending
+            .as_ref()
+            .and_then(|pending| pending.zone.clone())
+            .or_else(|| self.zone.clone())
+            .expect("zone must be set in either pending or root")
+    }
+}
+
+impl SdnZone {}
+
+impl SdnController {
+    /// returns the ASN from the pending property if it has a value, otherwise it returns self.asn
+    pub fn asn_pending(&self) -> Option<u32> {
+        self.pending
+            .as_ref()
+            .and_then(|pending| pending.asn)
+            .or(self.asn)
+    }
+}
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v4 01/15] server: add locked sdn client helpers
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (14 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-api-types v4 6/6] sdn: add helpers for pending values Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 02/15] ui: pve: sdn: add descriptions for sdn tasks Stefan Hanreich
                   ` (16 subsequent siblings)
  32 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

Add a new client that represents a remote with a locked SDN
configuration. It works by creating a new PveClient and then locking
the SDN configuration via the client. It ensures that, while the lock
is held, all methods are called with the proper lock secret. It also
provides helpers for applying / rollbacking the configuration and
releasing the lock.

Additionally, a collection type is introduced, that can hold multiple
locked SDN clients. It provides a helper for executing SDN API calls
across multiple remotes and rolling back the SDN configuration of all
remotes if any API command on any remote fails. This client will be
used for making changes across all remotes in PDM worker tasks.

After applying, we cannot rollback the configuration - so the
collection only logs any errors - waits for the execution to finish on
all remotes and then returns a Result indicating whether the operation
was successful on *all* remotes.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 server/src/lib.rs        |   1 +
 server/src/sdn_client.rs | 432 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 433 insertions(+)
 create mode 100644 server/src/sdn_client.rs

diff --git a/server/src/lib.rs b/server/src/lib.rs
index 3f8b770..33213e1 100644
--- a/server/src/lib.rs
+++ b/server/src/lib.rs
@@ -14,6 +14,7 @@ pub mod task_utils;
 
 pub mod connection;
 pub mod pbs_client;
+pub mod sdn_client;
 
 #[cfg(any(remote_config = "faked", test))]
 pub mod test_support;
diff --git a/server/src/sdn_client.rs b/server/src/sdn_client.rs
new file mode 100644
index 0000000..60ef0e0
--- /dev/null
+++ b/server/src/sdn_client.rs
@@ -0,0 +1,432 @@
+use std::sync::Arc;
+use std::time::Duration;
+
+use anyhow::{self, bail, Context};
+
+use futures::{future::join_all, stream::FuturesUnordered, StreamExt, TryFutureExt};
+use pdm_api_types::{remotes::Remote, RemoteUpid};
+use pve_api_types::{
+    client::PveClient, CreateSdnLock, CreateVnet, CreateZone, PveUpid, ReleaseSdnLock, ReloadSdn,
+    RollbackSdn,
+};
+
+use crate::api::pve::{connect, get_remote};
+
+/// Wrapper for [`PveClient`] for representing a locked SDN configuration.
+///
+/// It stores the client that has been locked, as well as the lock_token that is required for
+/// making changes to the SDN configuration. It provides methods that proxy the respective SDN
+/// endpoints, where it adds the lock_token when making the proxied calls.
+pub struct LockedSdnClient {
+    lock_token: String,
+    client: Arc<dyn PveClient + Send + Sync>,
+}
+
+#[derive(Debug)]
+pub enum LockedSdnClientError {
+    Client(proxmox_client::Error),
+    Other(anyhow::Error),
+}
+
+impl std::error::Error for LockedSdnClientError {}
+
+impl std::fmt::Display for LockedSdnClientError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::Client(err) => err.fmt(f),
+            Self::Other(err) => err.fmt(f),
+        }
+    }
+}
+
+impl From<proxmox_client::Error> for LockedSdnClientError {
+    fn from(value: proxmox_client::Error) -> Self {
+        Self::Client(value)
+    }
+}
+
+impl From<anyhow::Error> for LockedSdnClientError {
+    fn from(value: anyhow::Error) -> Self {
+        Self::Other(value)
+    }
+}
+
+impl LockedSdnClient {
+    /// Creates a new PveClient for a given [`Remote`] and locks the SDN configuration there.
+    ///
+    /// # Errors
+    ///
+    /// This function will return an error if locking the remote fails.
+    pub async fn new(
+        remote: &Remote,
+        allow_pending: impl Into<Option<bool>>,
+    ) -> Result<Self, LockedSdnClientError> {
+        let client = connect(remote).map_err(LockedSdnClientError::from)?;
+
+        let params = CreateSdnLock {
+            allow_pending: allow_pending.into(),
+        };
+
+        client
+            .acquire_sdn_lock(params)
+            .await
+            .map(|lock_token| Self { lock_token, client })
+            .map_err(LockedSdnClientError::from)
+    }
+
+    /// proxies [`PveClient::create_vnet`] and adds lock_token to the passed parameters before
+    /// making the call.
+    pub async fn create_vnet(&self, mut params: CreateVnet) -> Result<(), proxmox_client::Error> {
+        params.lock_token = Some(self.lock_token.clone());
+
+        self.client.create_vnet(params).await
+    }
+
+    /// proxies [`PveClient::create_zone`] and adds lock_token to the passed parameters before
+    /// making the call.
+    pub async fn create_zone(&self, mut params: CreateZone) -> Result<(), proxmox_client::Error> {
+        params.lock_token = Some(self.lock_token.clone());
+
+        self.client.create_zone(params).await
+    }
+
+    /// applies the changes made while the client was locked and returns the original [`PveClient`] if the
+    /// changes have been applied successfully.
+    pub async fn apply_and_release(
+        self,
+    ) -> Result<(PveUpid, Arc<dyn PveClient + Send + Sync>), proxmox_client::Error> {
+        let params = ReloadSdn {
+            lock_token: Some(self.lock_token.clone()),
+            release_lock: Some(true),
+        };
+
+        self.client
+            .sdn_apply(params)
+            .await
+            .map(move |upid| (upid, self.client))
+    }
+
+    /// releases the lock on the [`PveClient`] without applying pending changes.
+    pub async fn release(
+        self,
+        force: impl Into<Option<bool>>,
+    ) -> Result<Arc<dyn PveClient + Send + Sync>, proxmox_client::Error> {
+        let params = ReleaseSdnLock {
+            force: force.into(),
+            lock_token: Some(self.lock_token),
+        };
+
+        self.client.release_sdn_lock(params).await?;
+        Ok(self.client)
+    }
+
+    /// rolls back all pending changes and then releases the lock
+    pub async fn rollback_and_release(
+        self,
+    ) -> Result<Arc<dyn PveClient + Send + Sync>, proxmox_client::Error> {
+        let params = RollbackSdn {
+            lock_token: Some(self.lock_token),
+            release_lock: Some(true),
+        };
+
+        self.client.rollback_sdn_changes(params).await?;
+        Ok(self.client)
+    }
+}
+
+/// Context for [`LockedSdnClient`] stored in [`LockedSdnClients`].
+pub struct LockedSdnClientContext<C> {
+    remote_id: String,
+    data: C,
+}
+
+impl<C> LockedSdnClientContext<C> {
+    fn new(remote_id: String, data: C) -> Self {
+        Self { remote_id, data }
+    }
+
+    pub fn remote_id(&self) -> &str {
+        &self.remote_id
+    }
+
+    pub fn data(&self) -> &C {
+        &self.data
+    }
+}
+
+/// A collection abstracting [`LockedSdnClient`] for multiple locked remotes.
+///
+/// It can be used for running the same command across multiple remotes, while automatically
+/// handling rollback and releasing locks in case of failures across all remotes. If an API call
+/// made to one of the remotes fails, then this client will automatically take care of rolling back
+/// all changes made during the transaction and then releasing the locks.
+pub struct LockedSdnClients<C> {
+    clients: Vec<(LockedSdnClient, LockedSdnClientContext<C>)>,
+}
+
+impl<C> LockedSdnClients<C> {
+    /// A convenience function for creating locked clients for multiple remotes.
+    ///
+    /// For each remote a Context can be specified, which will be supplied to all callbacks that
+    /// are using this [`LockedSdnClients`] to make calls across all remotes.
+    ///
+    /// # Errors
+    ///
+    /// This function will return an error if:
+    /// * the remote configuration cannot be read
+    /// * any of the supplied remotes is not contained in the configuration
+    /// * locking the configuration on any remote fails
+    ///
+    /// If necessary, the configuration of all remotes will be unlocked, if possible.
+    pub async fn from_remote_names<I: IntoIterator<Item = (String, C)>>(
+        remote_names: I,
+        allow_pending: bool,
+    ) -> Result<Self, anyhow::Error> {
+        let (remote_config, _) = pdm_config::remotes::config()?;
+
+        let mut clients = Vec::new();
+
+        for (remote_name, context) in remote_names {
+            let remote = get_remote(&remote_config, &remote_name)?;
+            proxmox_log::info!("acquiring lock for remote {}", remote.id);
+
+            match LockedSdnClient::new(remote, allow_pending).await {
+                Ok(client) => {
+                    let context = LockedSdnClientContext::new(remote_name, context);
+                    clients.push((client, context));
+                }
+                Err(error) => {
+                    proxmox_log::info!(
+                        "encountered an error when locking a remote, releasing all locks"
+                    );
+
+                    for (client, ctx) in clients {
+                        proxmox_log::info!("releasing lock for remote {}", ctx.remote_id);
+
+                        if let Err(error) = client.release(false).await {
+                            proxmox_log::error!(
+                                "could not release lock for remote {}: {error:#}",
+                                remote.id
+                            )
+                        }
+                    }
+
+                    return match &error {
+                        LockedSdnClientError::Client(proxmox_client::Error::Api(status, _msg))
+                            if *status == 501 =>
+                        {
+                            bail!("remote {} does not support the sdn locking api, please upgrade to PVE 9 or newer!", remote.id)
+                        }
+                        _ => Err(error).with_context(|| {
+                            format!("could not lock sdn configuration for remote {}", remote.id)
+                        }),
+                    };
+                }
+            };
+        }
+
+        clients.sort_by(|(_, ctx_a), (_, ctx_b)| ctx_a.remote_id.cmp(&ctx_b.remote_id));
+
+        Ok(Self { clients })
+    }
+
+    /// Executes the given callback for each [`LockedSdnClient`] in this collection.
+    ///
+    /// On error, it tries to rollback the configuration of *all* locked clients, releases the lock
+    /// and returns the error. If rollbacking fails, an error will be logged and no further action
+    /// is taken.
+    pub async fn for_each<F>(self, callback: F) -> Result<Self, anyhow::Error>
+    where
+        F: AsyncFn(
+            &LockedSdnClient,
+            &LockedSdnClientContext<C>,
+        ) -> Result<(), proxmox_client::Error>,
+    {
+        let futures = self.clients.iter().map(|(client, context)| {
+            callback(client, context)
+                .map_ok(|_| context.remote_id())
+                .map_err(|err| (err, context.remote_id()))
+        });
+
+        let mut errors = false;
+
+        for result in join_all(futures).await {
+            match result {
+                Ok(remote_id) => {
+                    proxmox_log::info!("succcessfully executed transaction on remote {remote_id}");
+                }
+                Err((error, remote_id)) => {
+                    proxmox_log::error!(
+                        "failed to execute transaction on remote {remote_id}: {error:#}",
+                    );
+                    errors = true;
+                }
+            }
+        }
+
+        if errors {
+            let mut rollback_futures = FuturesUnordered::new();
+
+            for (client, ctx) in self.clients {
+                let ctx = Arc::new(ctx);
+                let err_ctx = ctx.clone();
+
+                rollback_futures.push(
+                    client
+                        .rollback_and_release()
+                        .map_ok(|_| ctx)
+                        .map_err(|err| (err, err_ctx)),
+                );
+            }
+
+            while let Some(result) = rollback_futures.next().await {
+                match result {
+                    // older versions of PVE 9 potentially return 1 instead of an empty body, which
+                    // can trigger an BadApi Error in the client. Ignore the error here to work around
+                    // this issue.
+                    Ok(ctx) | Err((proxmox_client::Error::BadApi(_, _), ctx)) => {
+                        proxmox_log::info!(
+                            "successfully rolled back configuration for remote {}",
+                            ctx.remote_id()
+                        )
+                    }
+                    Err((_, ctx)) => {
+                        proxmox_log::error!(
+                            "could not rollback and unlock configuration for remote {} - configuration needs to be manually unlocked via 'pvesh delete /cluster/sdn/lock --force 1'",
+                            ctx.remote_id()
+                        )
+                    }
+                }
+            }
+
+            bail!("running the transaction failed on at least one remote!");
+        }
+
+        Ok(self)
+    }
+
+    // pve-http-server TCP connection timeout is 5 seconds, use a lower amount with some margin for
+    // latency in order to avoid re-opening TCP connections for every polling request.
+    const POLLING_INTERVAL: Duration = Duration::from_secs(3);
+
+    /// Convenience function for polling a running task on a PVE remote.
+    ///
+    /// It polls a given task on a given node, waiting for the task to finish successfully.
+    ///
+    /// # Errors
+    ///
+    /// This function will return an error if:
+    /// * There was a problem querying the task status (this does not necessarily mean the task failed).
+    /// * The task finished unsuccessfully.
+    async fn poll_task(
+        node: String,
+        upid: RemoteUpid,
+        client: Arc<dyn PveClient + Send + Sync>,
+    ) -> Result<RemoteUpid, anyhow::Error> {
+        loop {
+            tokio::time::sleep(Self::POLLING_INTERVAL).await;
+
+            let status = client.get_task_status(&node, &upid.upid).await?;
+
+            if !status.is_running() {
+                if status.finished_successfully() == Some(true) {
+                    return Ok(upid);
+                } else {
+                    bail!(
+                        "task did not finish successfully on remote {}",
+                        upid.remote()
+                    );
+                }
+            }
+        }
+    }
+
+    /// Applies and Reloads the SDN configuration for all locked clients.
+    ///
+    /// This function tries to apply the SDN configuration for all supplied locked clients and, if
+    /// it was successful, to reload the SDN configuration of the remote. It logs success and error
+    /// messages via proxmox_log. Rollbacking in cases of failure is no longer possible, so this
+    /// function then returns an error if applying or reloading the configuration was unsuccessful
+    /// on at least one remote.
+    ///
+    /// # Errors This function returns an error if applying or reloading the configuration on one
+    /// of the remotes failed. It will always wait for all futures to finish and only return an
+    /// error afterwards.
+    pub async fn apply_and_release(self) -> Result<(), anyhow::Error> {
+        let mut futures = FuturesUnordered::new();
+
+        for (client, context) in self.clients {
+            let ctx = Arc::new(context);
+            let err_ctx = ctx.clone();
+
+            futures.push(
+                client
+                    .apply_and_release()
+                    .map_ok(|(upid, client)| ((upid, client), ctx))
+                    .map_err(|err| (err, err_ctx)),
+            );
+        }
+
+        let mut reload_futures = FuturesUnordered::new();
+
+        while let Some(result) = futures.next().await {
+            match result {
+                Ok(((upid, client), ctx)) => {
+                    proxmox_log::info!(
+                        "successfully applied sdn config on remote {}",
+                        ctx.remote_id()
+                    );
+
+                    let Ok(remote_upid) =
+                        RemoteUpid::try_from((ctx.remote_id(), upid.to_string().as_str()))
+                    else {
+                        proxmox_log::error!("invalid UPID received from PVE: {upid}");
+                        continue;
+                    };
+
+                    reload_futures.push(
+                        Self::poll_task(upid.node.clone(), remote_upid, client)
+                            .map_err(move |err| (err, ctx)),
+                    );
+                }
+                Err((error, ctx)) => {
+                    proxmox_log::error!(
+                        "failed to apply sdn configuration on remote {}: {error:#}, not reloading",
+                        ctx.remote_id()
+                    );
+                }
+            }
+        }
+
+        proxmox_log::info!(
+            "Waiting for reload tasks to finish on all remotes, this can take awhile"
+        );
+
+        let mut errors = false;
+
+        while let Some(result) = reload_futures.next().await {
+            match result {
+                Ok(upid) => {
+                    proxmox_log::info!(
+                        "successfully reloaded configuration on remote {}",
+                        upid.remote()
+                    );
+                }
+                Err((error, ctx)) => {
+                    proxmox_log::error!(
+                        "could not reload configuration on remote {}: {error:#}",
+                        ctx.remote_id()
+                    );
+
+                    errors = true;
+                }
+            }
+        }
+
+        if errors {
+            bail!("failed to apply configuration on at least one remote");
+        }
+
+        Ok(())
+    }
+}
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v4 02/15] ui: pve: sdn: add descriptions for sdn tasks
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (15 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 01/15] server: add locked sdn client helpers Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 03/15] api: sdn: add list_zones endpoint Stefan Hanreich
                   ` (15 subsequent siblings)
  32 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 ui/src/tasks.rs | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/ui/src/tasks.rs b/ui/src/tasks.rs
index 6aa202a..7192f6c 100644
--- a/ui/src/tasks.rs
+++ b/ui/src/tasks.rs
@@ -28,6 +28,8 @@ pub fn register_pve_tasks() {
     register_task_description("cephsetflags", tr!("Change global Ceph flags"));
     register_task_description("clustercreate", tr!("Create Cluster"));
     register_task_description("clusterjoin", tr!("Join Cluster"));
+    register_task_description("create_zone", tr!("Create EVPN Zone"));
+    register_task_description("create_vnet", tr!("Create EVPN VNet"));
     register_task_description("dircreate", (tr!("Directory Storage"), tr!("Create")));
     register_task_description("dirremove", (tr!("Directory"), tr!("Remove")));
     register_task_description("download", (tr!("File"), tr!("Download")));
@@ -65,9 +67,11 @@ pub fn register_pve_tasks() {
     register_task_description("qmstop", ("VM", tr!("Stop")));
     register_task_description("qmsuspend", ("VM", tr!("Hibernate")));
     register_task_description("qmtemplate", ("VM", tr!("Convert to template")));
+    register_task_description("reloadnetworkall", tr!("Apply SDN configuration"));
     register_task_description("resize", ("VM/CT", tr!("Resize")));
     register_task_description("spiceproxy", ("VM/CT", tr!("Console") + " (Spice)"));
     register_task_description("spiceshell", tr!("Shell") + " (Spice)");
+    register_task_description("srvreload", tr!("Reload network configuration"));
     register_task_description("startall", tr!("Bulk start VMs and Containers"));
     register_task_description("stopall", tr!("Bulk shutdown VMs and Containers"));
     register_task_description("suspendall", tr!("Suspend all VMs"));
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v4 03/15] api: sdn: add list_zones endpoint
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (16 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 02/15] ui: pve: sdn: add descriptions for sdn tasks Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 04/15] api: sdn: add create_zone endpoint Stefan Hanreich
                   ` (14 subsequent siblings)
  32 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

Add an endpoint for listing the zones of all configured PVE remotes.
They can be filtered by type / remote and it exposes options for
querying the pending / running configuration.

This call is quite expensive, since it makes a GET call to every
configured PVE remote, which can take awhile depending on the network
connection. For the future we might want to introduce a caching
mechanism for this call, but we've decided against it for the time
being.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 lib/pdm-api-types/Cargo.toml |   2 +
 lib/pdm-api-types/src/lib.rs |   2 +
 lib/pdm-api-types/src/sdn.rs |  24 ++++++++
 lib/pdm-client/src/lib.rs    |  18 ++++++
 server/src/api/mod.rs        |   2 +
 server/src/api/sdn/mod.rs    |  13 ++++
 server/src/api/sdn/zones.rs  | 113 +++++++++++++++++++++++++++++++++++
 7 files changed, 174 insertions(+)
 create mode 100644 lib/pdm-api-types/src/sdn.rs
 create mode 100644 server/src/api/sdn/mod.rs
 create mode 100644 server/src/api/sdn/zones.rs

diff --git a/lib/pdm-api-types/Cargo.toml b/lib/pdm-api-types/Cargo.toml
index 26f7227..015df51 100644
--- a/lib/pdm-api-types/Cargo.toml
+++ b/lib/pdm-api-types/Cargo.toml
@@ -23,3 +23,5 @@ proxmox-dns-api.workspace = true
 proxmox-time.workspace = true
 proxmox-serde.workspace = true
 proxmox-subscription = { workspace = true, features = ["api-types"], default-features = false }
+
+pve-api-types = { workspace = true }
diff --git a/lib/pdm-api-types/src/lib.rs b/lib/pdm-api-types/src/lib.rs
index 8dbacba..07d18a9 100644
--- a/lib/pdm-api-types/src/lib.rs
+++ b/lib/pdm-api-types/src/lib.rs
@@ -101,6 +101,8 @@ pub mod rrddata;
 
 pub mod subscription;
 
+pub mod sdn;
+
 const_regex! {
     // just a rough check - dummy acceptor is used before persisting
     pub OPENSSL_CIPHERS_REGEX = r"^[0-9A-Za-z_:, +!\-@=.]+$";
diff --git a/lib/pdm-api-types/src/sdn.rs b/lib/pdm-api-types/src/sdn.rs
new file mode 100644
index 0000000..28b20c5
--- /dev/null
+++ b/lib/pdm-api-types/src/sdn.rs
@@ -0,0 +1,24 @@
+use proxmox_schema::{api, const_regex, ApiStringFormat, IntegerSchema, Schema, StringSchema};
+use pve_api_types::SdnZone;
+use serde::{Deserialize, Serialize};
+
+use crate::remotes::REMOTE_ID_SCHEMA;
+
+#[api(
+    properties: {
+        remote: {
+            schema: REMOTE_ID_SCHEMA,
+        },
+        zone: {
+            type: SdnZone,
+            flatten: true,
+        }
+    }
+)]
+/// SDN controller with additional information about which remote it belongs to
+#[derive(Serialize, Deserialize, Clone, PartialEq)]
+pub struct ListZone {
+    pub remote: String,
+    #[serde(flatten)]
+    pub zone: SdnZone,
+}
diff --git a/lib/pdm-client/src/lib.rs b/lib/pdm-client/src/lib.rs
index f2bb546..5f7f18c 100644
--- a/lib/pdm-client/src/lib.rs
+++ b/lib/pdm-client/src/lib.rs
@@ -8,6 +8,7 @@ use pdm_api_types::resource::{PveResource, RemoteResources, TopEntities};
 use pdm_api_types::rrddata::{
     LxcDataPoint, NodeDataPoint, PbsDatastoreDataPoint, PbsNodeDataPoint, QemuDataPoint,
 };
+use pdm_api_types::sdn::ListZone;
 use pdm_api_types::BasicRealmInfo;
 use pve_api_types::StartQemuMigrationType;
 use serde::{Deserialize, Serialize};
@@ -57,6 +58,8 @@ pub mod types {
     pub use pve_api_types::ClusterNodeStatus;
 
     pub use pve_api_types::PveUpid;
+
+    pub use pve_api_types::ListZonesType;
 }
 
 pub struct PdmClient<T: HttpApiClient>(pub T);
@@ -966,6 +969,21 @@ impl<T: HttpApiClient> PdmClient<T> {
             .expect_json()?
             .data)
     }
+
+    pub async fn pve_sdn_list_zones(
+        &self,
+        pending: impl Into<Option<bool>>,
+        running: impl Into<Option<bool>>,
+        ty: impl Into<Option<ListZonesType>>,
+    ) -> Result<Vec<ListZone>, Error> {
+        let path = ApiPathBuilder::new("/api2/extjs/sdn/zones".to_string())
+            .maybe_arg("pending", &pending.into())
+            .maybe_arg("running", &running.into())
+            .maybe_arg("ty", &ty.into())
+            .build();
+
+        Ok(self.0.get(&path).await?.expect_json()?.data)
+    }
 }
 
 /// Builder for migration parameters.
diff --git a/server/src/api/mod.rs b/server/src/api/mod.rs
index ff875fc..02ee0ec 100644
--- a/server/src/api/mod.rs
+++ b/server/src/api/mod.rs
@@ -17,6 +17,7 @@ pub mod remote_tasks;
 pub mod remotes;
 pub mod resources;
 mod rrd_common;
+pub mod sdn;
 
 #[sortable]
 const SUBDIRS: SubdirMap = &sorted!([
@@ -30,6 +31,7 @@ const SUBDIRS: SubdirMap = &sorted!([
     ("resources", &resources::ROUTER),
     ("nodes", &nodes::ROUTER),
     ("remote-tasks", &remote_tasks::ROUTER),
+    ("sdn", &sdn::ROUTER),
     ("version", &Router::new().get(&API_METHOD_VERSION)),
 ]);
 
diff --git a/server/src/api/sdn/mod.rs b/server/src/api/sdn/mod.rs
new file mode 100644
index 0000000..2abdaf6
--- /dev/null
+++ b/server/src/api/sdn/mod.rs
@@ -0,0 +1,13 @@
+use proxmox_router::{list_subdirs_api_method, Router, SubdirMap};
+use proxmox_sortable_macro::sortable;
+
+pub mod zones;
+
+#[sortable]
+pub const SUBDIRS: SubdirMap = &sorted!([
+    ("zones", &zones::ROUTER),
+]);
+
+pub const ROUTER: Router = Router::new()
+    .get(&list_subdirs_api_method!(SUBDIRS))
+    .subdirs(SUBDIRS);
diff --git a/server/src/api/sdn/zones.rs b/server/src/api/sdn/zones.rs
new file mode 100644
index 0000000..ce7f201
--- /dev/null
+++ b/server/src/api/sdn/zones.rs
@@ -0,0 +1,113 @@
+use std::collections::HashSet;
+
+use anyhow::{format_err, Error};
+
+use pbs_api_types::REMOTE_ID_SCHEMA;
+use pdm_api_types::{remotes::RemoteType, sdn::ListZone};
+use proxmox_router::Router;
+use proxmox_schema::api;
+use pve_api_types::ListZonesType;
+
+use crate::{
+    api::pve,
+    parallel_fetcher::{NodeResults, ParallelFetcher},
+    sdn_client::LockedSdnClients,
+};
+
+pub const ROUTER: Router = Router::new().get(&API_METHOD_LIST_ZONES);
+
+#[api(
+    input: {
+        properties: {
+            pending: {
+                type: Boolean,
+                optional: true,
+                description: "Include a list of attributes whose changes are currently pending.",
+            },
+            running: {
+                type: Boolean,
+                optional: true,
+                description: "If true shows the running configuration, otherwise the pending configuration.",
+            },
+            ty: {
+                type: ListZonesType,
+                optional: true,
+            },
+            remotes: {
+                type: Array,
+                optional: true,
+                description: "Only return controllers from the specified remotes.",
+                items: {
+                    schema: REMOTE_ID_SCHEMA,
+                }
+            },
+        }
+    },
+    returns: {
+        type: Array,
+        description: "Get a list of zones fitting the filtering criteria.",
+        items: {
+            type: ListZone,
+        },
+    },
+)]
+/// Query zones of remotes with optional filtering options
+pub async fn list_zones(
+    pending: Option<bool>,
+    running: Option<bool>,
+    ty: Option<ListZonesType>,
+    remotes: Option<HashSet<String>>,
+) -> Result<Vec<ListZone>, Error> {
+    let (remote_config, _) = pdm_config::remotes::config()?;
+
+    let filtered_remotes = remote_config.into_iter().filter_map(|(_, remote)| {
+        if remote.ty == RemoteType::Pve
+            && remotes
+                .as_ref()
+                .map(|remotes| remotes.contains(&remote.id))
+                .unwrap_or(true)
+        {
+            return Some(remote);
+        }
+
+        None
+    });
+
+    let mut vnets = Vec::new();
+    let fetcher = ParallelFetcher::new((pending, running, ty));
+
+    let results = fetcher
+        .do_for_all_remotes(filtered_remotes, async |ctx, r, _| {
+            Ok(pve::connect(&r)?.list_zones(ctx.0, ctx.1, ctx.2).await?)
+        })
+        .await;
+
+    for (remote, remote_result) in results.remote_results.into_iter() {
+        match remote_result {
+            Ok(remote_result) => {
+                for (node, node_result) in remote_result.node_results.into_iter() {
+                    match node_result {
+                        Ok(NodeResults { data, .. }) => {
+                            vnets.extend(data.into_iter().map(|zone| ListZone {
+                                remote: remote.clone(),
+                                zone,
+                            }))
+                        }
+                        Err(error) => {
+                            log::error!(
+                                "could not fetch vnets from remote {} node {}: {error:#}",
+                                remote,
+                                node
+                            );
+                        }
+                    }
+                }
+            }
+            Err(error) => {
+                log::error!("could not fetch vnets from remote {}: {error:#}", remote)
+            }
+        }
+    }
+
+    Ok(vnets)
+}
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v4 04/15] api: sdn: add create_zone endpoint
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (17 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 03/15] api: sdn: add list_zones endpoint Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 05/15] api: sdn: add list_vnets endpoint Stefan Hanreich
                   ` (13 subsequent siblings)
  32 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

This endpoint is used for creating a new EVPN zone on multiple
remotes. It utilizes the newly introduced LockSdnClients helper for
performing the action simultaneously across all remotes and rolling
back in case of failure.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 lib/pdm-api-types/src/sdn.rs |  64 ++++++++++++++++++++++
 lib/pdm-client/src/lib.rs    |   7 +++
 server/src/api/sdn/zones.rs  | 101 +++++++++++++++++++++++++++++++++--
 3 files changed, 168 insertions(+), 4 deletions(-)

diff --git a/lib/pdm-api-types/src/sdn.rs b/lib/pdm-api-types/src/sdn.rs
index 28b20c5..ca5a21e 100644
--- a/lib/pdm-api-types/src/sdn.rs
+++ b/lib/pdm-api-types/src/sdn.rs
@@ -4,6 +4,70 @@ use serde::{Deserialize, Serialize};
 
 use crate::remotes::REMOTE_ID_SCHEMA;
 
+pub const VXLAN_ID_SCHEMA: Schema = IntegerSchema::new("VXLAN VNI")
+    .minimum(1)
+    .maximum(16777215)
+    .schema();
+
+pub const SDN_ID_SCHEMA: Schema =
+    StringSchema::new("The name for an SDN object (zone / vnet / fabric).")
+        .format(&ApiStringFormat::VerifyFn(
+            pve_api_types::verifiers::verify_sdn_id,
+        ))
+        .schema();
+
+pub const SDN_CONTROLLER_ID_SCHEMA: Schema = StringSchema::new("The name for an SDN controller.")
+    .format(&ApiStringFormat::VerifyFn(
+        pve_api_types::verifiers::verify_sdn_controller_id,
+    ))
+    .schema();
+
+#[api(
+    properties: {
+        remote: {
+            schema: REMOTE_ID_SCHEMA,
+        },
+        controller: {
+            schema: SDN_CONTROLLER_ID_SCHEMA,
+        },
+    }
+)]
+/// Describes the remote-specific informations for creating a new zone.
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct CreateZoneRemote {
+    pub remote: String,
+    pub controller: String,
+}
+
+#[api(
+    properties: {
+        "vrf-vxlan": {
+            schema: VXLAN_ID_SCHEMA,
+            optional: true,
+        },
+        remotes: {
+            type: Array,
+            description: "List of remotes and the controllers with which the zone should get created.",
+            items: {
+                type: CreateZoneRemote,
+            }
+        },
+        zone: {
+            schema: SDN_ID_SCHEMA,
+        },
+    }
+)]
+/// Contains the information for creating a new zone as well as information about the remotes where
+/// the zone should get created.
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct CreateZoneParams {
+    pub zone: String,
+    pub vrf_vxlan: Option<u32>,
+    pub remotes: Vec<CreateZoneRemote>,
+}
+
 #[api(
     properties: {
         remote: {
diff --git a/lib/pdm-client/src/lib.rs b/lib/pdm-client/src/lib.rs
index 5f7f18c..9da18c9 100644
--- a/lib/pdm-client/src/lib.rs
+++ b/lib/pdm-client/src/lib.rs
@@ -59,6 +59,7 @@ pub mod types {
 
     pub use pve_api_types::PveUpid;
 
+    pub use pdm_api_types::sdn::{CreateZoneParams, ListZone};
     pub use pve_api_types::ListZonesType;
 }
 
@@ -984,6 +985,12 @@ impl<T: HttpApiClient> PdmClient<T> {
 
         Ok(self.0.get(&path).await?.expect_json()?.data)
     }
+
+    pub async fn pve_sdn_create_zone(&self, params: CreateZoneParams) -> Result<String, Error> {
+        let path = "/api2/extjs/sdn/zones";
+
+        Ok(self.0.post(path, &params).await?.expect_json()?.data)
+    }
 }
 
 /// Builder for migration parameters.
diff --git a/server/src/api/sdn/zones.rs b/server/src/api/sdn/zones.rs
index ce7f201..5e0ec54 100644
--- a/server/src/api/sdn/zones.rs
+++ b/server/src/api/sdn/zones.rs
@@ -3,10 +3,15 @@ use std::collections::HashSet;
 use anyhow::{format_err, Error};
 
 use pbs_api_types::REMOTE_ID_SCHEMA;
-use pdm_api_types::{remotes::RemoteType, sdn::ListZone};
-use proxmox_router::Router;
+use pdm_api_types::{
+    remotes::RemoteType,
+    sdn::{CreateZoneRemote, ListZone, SDN_ID_SCHEMA, VXLAN_ID_SCHEMA},
+    Authid,
+};
+use proxmox_rest_server::WorkerTask;
+use proxmox_router::{Router, RpcEnvironment};
 use proxmox_schema::api;
-use pve_api_types::ListZonesType;
+use pve_api_types::{CreateZone, ListZonesType};
 
 use crate::{
     api::pve,
@@ -14,7 +19,9 @@ use crate::{
     sdn_client::LockedSdnClients,
 };
 
-pub const ROUTER: Router = Router::new().get(&API_METHOD_LIST_ZONES);
+pub const ROUTER: Router = Router::new()
+    .get(&API_METHOD_LIST_ZONES)
+    .post(&API_METHOD_CREATE_ZONE);
 
 #[api(
     input: {
@@ -111,3 +118,89 @@ pub async fn list_zones(
 
     Ok(vnets)
 }
+
+#[api(
+    input: {
+        properties: {
+            zone: { schema: SDN_ID_SCHEMA },
+            "vrf-vxlan": {
+                schema: VXLAN_ID_SCHEMA,
+                optional: true,
+            },
+            remotes: {
+                type: Array,
+                description: "List of remotes with their controller where zone should get created.",
+                items: {
+                    type: CreateZoneRemote
+                }
+            },
+        },
+    },
+    returns: { type: String, description: "Worker UPID" },
+)]
+/// Create a zone across multiple remotes
+async fn create_zone(
+    zone: String,
+    vrf_vxlan: Option<u32>,
+    remotes: Vec<CreateZoneRemote>,
+    rpcenv: &mut dyn RpcEnvironment,
+) -> Result<String, Error> {
+    let auth_id: Authid = rpcenv
+        .get_auth_id()
+        .ok_or_else(|| format_err!("no authid available"))?
+        .parse()?;
+
+    let upid = WorkerTask::spawn(
+        "create_zone",
+        None,
+        auth_id.to_string(),
+        false,
+        move |_worker| async move {
+            LockedSdnClients::from_remote_names(
+                remotes
+                    .into_iter()
+                    .map(|remote| (remote.remote.clone(), remote)),
+                false,
+            )
+            .await?
+            .for_each(async move |client, ctx| {
+                let params = CreateZone {
+                    zone: zone.clone(),
+                    vrf_vxlan,
+                    controller: Some(ctx.data().controller.clone()),
+                    ty: ListZonesType::Evpn,
+                    advertise_subnets: None,
+                    bridge: None,
+                    bridge_disable_mac_learning: None,
+                    dhcp: None,
+                    disable_arp_nd_suppression: None,
+                    dns: None,
+                    dnszone: None,
+                    dp_id: None,
+                    exitnodes: None,
+                    exitnodes_local_routing: None,
+                    exitnodes_primary: None,
+                    ipam: None,
+                    mac: None,
+                    mtu: None,
+                    nodes: None,
+                    peers: None,
+                    reversedns: None,
+                    rt_import: None,
+                    tag: None,
+                    vlan_protocol: None,
+                    vxlan_port: None,
+                    lock_token: None,
+                    fabric: None,
+                };
+
+                client.create_zone(params).await
+            })
+            .await?
+            .apply_and_release()
+            .await
+        },
+    )?;
+
+    Ok(upid)
+}
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v4 05/15] api: sdn: add list_vnets endpoint
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (18 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 04/15] api: sdn: add create_zone endpoint Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 06/15] api: sdn: add create_vnet endpoint Stefan Hanreich
                   ` (12 subsequent siblings)
  32 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

Add an endpoint for listing the vnets of all configured PVE remotes.
They can be filtered by type / remote and it exposes options for
querying the pending / running configuration.

This call is quite expensive, since it makes a GET call to every
configured PVE remote, which can take awhile depending on the network
connection. For the future we might want to introduce a caching
mechanism for this call, but for now we decided against it.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 lib/pdm-api-types/src/sdn.rs |  21 ++++++-
 lib/pdm-client/src/lib.rs    |  15 ++++-
 server/src/api/sdn/mod.rs    |   2 +
 server/src/api/sdn/vnets.rs  | 107 +++++++++++++++++++++++++++++++++++
 4 files changed, 143 insertions(+), 2 deletions(-)
 create mode 100644 server/src/api/sdn/vnets.rs

diff --git a/lib/pdm-api-types/src/sdn.rs b/lib/pdm-api-types/src/sdn.rs
index ca5a21e..c102bba 100644
--- a/lib/pdm-api-types/src/sdn.rs
+++ b/lib/pdm-api-types/src/sdn.rs
@@ -1,5 +1,5 @@
 use proxmox_schema::{api, const_regex, ApiStringFormat, IntegerSchema, Schema, StringSchema};
-use pve_api_types::SdnZone;
+use pve_api_types::{SdnVnet, SdnZone};
 use serde::{Deserialize, Serialize};
 
 use crate::remotes::REMOTE_ID_SCHEMA;
@@ -68,6 +68,25 @@ pub struct CreateZoneParams {
     pub remotes: Vec<CreateZoneRemote>,
 }
 
+#[api(
+    properties: {
+        remote: {
+            schema: REMOTE_ID_SCHEMA,
+        },
+        vnet: {
+            type: pve_api_types::SdnVnet,
+            flatten: true,
+        }
+    }
+)]
+/// SDN controller with additional information about which remote it belongs to
+#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
+pub struct ListVnet {
+    pub remote: String,
+    #[serde(flatten)]
+    pub vnet: SdnVnet,
+}
+
 #[api(
     properties: {
         remote: {
diff --git a/lib/pdm-client/src/lib.rs b/lib/pdm-client/src/lib.rs
index 9da18c9..37961be 100644
--- a/lib/pdm-client/src/lib.rs
+++ b/lib/pdm-client/src/lib.rs
@@ -8,7 +8,7 @@ use pdm_api_types::resource::{PveResource, RemoteResources, TopEntities};
 use pdm_api_types::rrddata::{
     LxcDataPoint, NodeDataPoint, PbsDatastoreDataPoint, PbsNodeDataPoint, QemuDataPoint,
 };
-use pdm_api_types::sdn::ListZone;
+use pdm_api_types::sdn::{ListVnet, ListZone};
 use pdm_api_types::BasicRealmInfo;
 use pve_api_types::StartQemuMigrationType;
 use serde::{Deserialize, Serialize};
@@ -986,6 +986,19 @@ impl<T: HttpApiClient> PdmClient<T> {
         Ok(self.0.get(&path).await?.expect_json()?.data)
     }
 
+    pub async fn pve_sdn_list_vnets(
+        &self,
+        pending: impl Into<Option<bool>>,
+        running: impl Into<Option<bool>>,
+    ) -> Result<Vec<ListVnet>, Error> {
+        let path = ApiPathBuilder::new("/api2/extjs/sdn/vnets".to_string())
+            .maybe_arg("pending", &pending.into())
+            .maybe_arg("running", &running.into())
+            .build();
+
+        Ok(self.0.get(&path).await?.expect_json()?.data)
+    }
+
     pub async fn pve_sdn_create_zone(&self, params: CreateZoneParams) -> Result<String, Error> {
         let path = "/api2/extjs/sdn/zones";
 
diff --git a/server/src/api/sdn/mod.rs b/server/src/api/sdn/mod.rs
index 2abdaf6..ccf7123 100644
--- a/server/src/api/sdn/mod.rs
+++ b/server/src/api/sdn/mod.rs
@@ -1,10 +1,12 @@
 use proxmox_router::{list_subdirs_api_method, Router, SubdirMap};
 use proxmox_sortable_macro::sortable;
 
+pub mod vnets;
 pub mod zones;
 
 #[sortable]
 pub const SUBDIRS: SubdirMap = &sorted!([
+    ("vnets", &vnets::ROUTER),
     ("zones", &zones::ROUTER),
 ]);
 
diff --git a/server/src/api/sdn/vnets.rs b/server/src/api/sdn/vnets.rs
new file mode 100644
index 0000000..899c486
--- /dev/null
+++ b/server/src/api/sdn/vnets.rs
@@ -0,0 +1,107 @@
+use std::collections::HashSet;
+
+use anyhow::{format_err, Error};
+
+use pbs_api_types::REMOTE_ID_SCHEMA;
+use pdm_api_types::{remotes::RemoteType, sdn::ListVnet};
+use proxmox_router::Router;
+use proxmox_schema::api;
+
+use crate::{
+    api::pve,
+    parallel_fetcher::{NodeResults, ParallelFetcher},
+    sdn_client::LockedSdnClients,
+};
+
+pub const ROUTER: Router = Router::new().get(&API_METHOD_LIST_VNETS);
+
+#[api(
+    input: {
+        properties: {
+            pending: {
+                type: Boolean,
+                optional: true,
+                description: "Include a list of attributes whose changes are currently pending.",
+            },
+            running: {
+                type: Boolean,
+                optional: true,
+                description: "If true shows the running configuration, otherwise the pending configuration.",
+            },
+            remotes: {
+                type: Array,
+                optional: true,
+                description: "Only return controllers from the specified remotes.",
+                items: {
+                    schema: REMOTE_ID_SCHEMA,
+                }
+            },
+        }
+    },
+    returns: {
+        type: Array,
+        description: "Get a list of controllers fitting the filtering criteria.",
+        items: {
+            type: ListVnet,
+        },
+    },
+)]
+/// Query VNets of PVE remotes with optional filtering options
+async fn list_vnets(
+    pending: Option<bool>,
+    running: Option<bool>,
+    remotes: Option<HashSet<String>>,
+) -> Result<Vec<ListVnet>, Error> {
+    let (remote_config, _) = pdm_config::remotes::config()?;
+
+    let filtered_remotes = remote_config.into_iter().filter_map(|(_, remote)| {
+        if remote.ty == RemoteType::Pve
+            && remotes
+                .as_ref()
+                .map(|remotes| remotes.contains(&remote.id))
+                .unwrap_or(true)
+        {
+            return Some(remote);
+        }
+
+        None
+    });
+
+    let mut vnets = Vec::new();
+    let fetcher = ParallelFetcher::new((pending, running));
+
+    let results = fetcher
+        .do_for_all_remotes(filtered_remotes, async |ctx, r, _| {
+            Ok(pve::connect(&r)?.list_vnets(ctx.0, ctx.1).await?)
+        })
+        .await;
+
+    for (remote, remote_result) in results.remote_results.into_iter() {
+        match remote_result {
+            Ok(remote_result) => {
+                for (node, node_result) in remote_result.node_results.into_iter() {
+                    match node_result {
+                        Ok(NodeResults { data, .. }) => {
+                            vnets.extend(data.into_iter().map(|vnet| ListVnet {
+                                remote: remote.clone(),
+                                vnet,
+                            }))
+                        }
+                        Err(error) => {
+                            log::error!(
+                                "could not fetch vnets from remote {} node {}: {error:#}",
+                                remote,
+                                node
+                            );
+                        }
+                    }
+                }
+            }
+            Err(error) => {
+                log::error!("could not fetch vnets from remote {}: {error:#}", remote)
+            }
+        }
+    }
+
+    Ok(vnets)
+}
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v4 06/15] api: sdn: add create_vnet endpoint
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (19 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 05/15] api: sdn: add list_vnets endpoint Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 07/15] api: sdn: add list_controllers endpoint Stefan Hanreich
                   ` (11 subsequent siblings)
  32 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

This endpoint is used for creating a new EVPN vnet on multiple
remotes. It utilizes the newly introduced LockSdnClients helper for
performing the action simultaneously across all remotes and rolling
back in case of failure.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 lib/pdm-api-types/src/sdn.rs | 45 ++++++++++++++++++++
 lib/pdm-client/src/lib.rs    |  8 +++-
 server/src/api/sdn/vnets.rs  | 81 ++++++++++++++++++++++++++++++++++--
 3 files changed, 129 insertions(+), 5 deletions(-)

diff --git a/lib/pdm-api-types/src/sdn.rs b/lib/pdm-api-types/src/sdn.rs
index c102bba..a291c11 100644
--- a/lib/pdm-api-types/src/sdn.rs
+++ b/lib/pdm-api-types/src/sdn.rs
@@ -68,6 +68,51 @@ pub struct CreateZoneParams {
     pub remotes: Vec<CreateZoneRemote>,
 }
 
+#[api(
+    properties: {
+        remote: {
+            schema: REMOTE_ID_SCHEMA,
+        },
+        zone: {
+            schema: SDN_ID_SCHEMA,
+        },
+    }
+)]
+/// Describes the remote-specific informations for creating a new vnet.
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct CreateVnetRemote {
+    pub remote: String,
+    pub zone: String,
+}
+
+#[api(
+    properties: {
+        tag: {
+            schema: VXLAN_ID_SCHEMA,
+        },
+        remotes: {
+            type: Array,
+            description: "List of remotes and the zones in which the vnet should get created.",
+            items: {
+                type: CreateVnetRemote,
+            }
+        },
+        vnet: {
+            schema: SDN_ID_SCHEMA,
+        },
+    }
+)]
+/// Contains the information for creating a new vnet as well as information about the remotes where
+/// the vnet should get created.
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct CreateVnetParams {
+    pub tag: u32,
+    pub vnet: String,
+    pub remotes: Vec<CreateVnetRemote>,
+}
+
 #[api(
     properties: {
         remote: {
diff --git a/lib/pdm-client/src/lib.rs b/lib/pdm-client/src/lib.rs
index 37961be..a17a045 100644
--- a/lib/pdm-client/src/lib.rs
+++ b/lib/pdm-client/src/lib.rs
@@ -59,7 +59,7 @@ pub mod types {
 
     pub use pve_api_types::PveUpid;
 
-    pub use pdm_api_types::sdn::{CreateZoneParams, ListZone};
+    pub use pdm_api_types::sdn::{CreateVnetParams, CreateZoneParams, ListVnet, ListZone};
     pub use pve_api_types::ListZonesType;
 }
 
@@ -1004,6 +1004,12 @@ impl<T: HttpApiClient> PdmClient<T> {
 
         Ok(self.0.post(path, &params).await?.expect_json()?.data)
     }
+
+    pub async fn pve_sdn_create_vnet(&self, params: CreateVnetParams) -> Result<String, Error> {
+        let path = "/api2/extjs/sdn/vnets";
+
+        Ok(self.0.post(path, &params).await?.expect_json()?.data)
+    }
 }
 
 /// Builder for migration parameters.
diff --git a/server/src/api/sdn/vnets.rs b/server/src/api/sdn/vnets.rs
index 899c486..a8092cf 100644
--- a/server/src/api/sdn/vnets.rs
+++ b/server/src/api/sdn/vnets.rs
@@ -1,11 +1,16 @@
 use std::collections::HashSet;
 
 use anyhow::{format_err, Error};
-
 use pbs_api_types::REMOTE_ID_SCHEMA;
-use pdm_api_types::{remotes::RemoteType, sdn::ListVnet};
-use proxmox_router::Router;
+use pdm_api_types::{
+    remotes::RemoteType,
+    sdn::{CreateVnetRemote, ListVnet, SDN_ID_SCHEMA, VXLAN_ID_SCHEMA},
+    Authid,
+};
+use proxmox_rest_server::WorkerTask;
+use proxmox_router::{Router, RpcEnvironment};
 use proxmox_schema::api;
+use pve_api_types::{CreateVnet, SdnVnetType};
 
 use crate::{
     api::pve,
@@ -13,7 +18,9 @@ use crate::{
     sdn_client::LockedSdnClients,
 };
 
-pub const ROUTER: Router = Router::new().get(&API_METHOD_LIST_VNETS);
+pub const ROUTER: Router = Router::new()
+    .get(&API_METHOD_LIST_VNETS)
+    .post(&API_METHOD_CREATE_VNET);
 
 #[api(
     input: {
@@ -105,3 +112,69 @@ async fn list_vnets(
 
     Ok(vnets)
 }
+
+#[api(
+    input: {
+        properties: {
+            vnet: { schema: SDN_ID_SCHEMA },
+            tag: { schema: VXLAN_ID_SCHEMA, optional: true },
+            remotes: {
+                type: Array,
+                description: "List of remotes with the zone in which the VNet should get created.",
+                items: {
+                    type: CreateVnetRemote,
+                }
+            },
+        },
+    },
+    returns: { type: String, description: "Worker UPID" },
+)]
+/// Create a VNet across multiple remotes
+async fn create_vnet(
+    vnet: String,
+    tag: Option<u32>,
+    remotes: Vec<CreateVnetRemote>,
+    rpcenv: &mut dyn RpcEnvironment,
+) -> Result<String, Error> {
+    let auth_id: Authid = rpcenv
+        .get_auth_id()
+        .ok_or_else(|| format_err!("no authid available"))?
+        .parse()?;
+
+    let upid = WorkerTask::spawn(
+        "create_vnet",
+        None,
+        auth_id.to_string(),
+        false,
+        move |_worker| async move {
+            LockedSdnClients::from_remote_names(
+                remotes
+                    .into_iter()
+                    .map(|remote| (remote.remote.clone(), remote)),
+                false,
+            )
+            .await?
+            .for_each(async move |client, ctx| {
+                proxmox_log::info!("creating vnet {} on remote {}", vnet, ctx.remote_id());
+
+                let params = CreateVnet {
+                    alias: None,
+                    isolate_ports: None,
+                    tag,
+                    ty: Some(SdnVnetType::Vnet),
+                    vlanaware: None,
+                    vnet: vnet.to_string(),
+                    zone: ctx.data().zone.clone(),
+                    lock_token: None,
+                };
+
+                client.create_vnet(params).await
+            })
+            .await?
+            .apply_and_release()
+            .await
+        },
+    )?;
+
+    Ok(upid)
+}
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v4 07/15] api: sdn: add list_controllers endpoint
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (20 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 06/15] api: sdn: add create_vnet endpoint Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 08/15] ui: sdn: add EvpnRouteTarget type Stefan Hanreich
                   ` (10 subsequent siblings)
  32 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

Add an endpoint for listing the controllers of all configured PVE
remotes. They can be filtered by type / remote and it exposes options
for querying the pending / running configuration.

This call is quite expensive, since it makes a GET call to every
configured PVE remote, which can take awhile depending on the network
connection. For the future we might want to introduce a caching
mechanism for this call.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 lib/pdm-api-types/src/sdn.rs      |  23 +++++-
 lib/pdm-client/src/lib.rs         |  21 +++++-
 server/src/api/sdn/controllers.rs | 114 ++++++++++++++++++++++++++++++
 server/src/api/sdn/mod.rs         |   2 +
 4 files changed, 156 insertions(+), 4 deletions(-)
 create mode 100644 server/src/api/sdn/controllers.rs

diff --git a/lib/pdm-api-types/src/sdn.rs b/lib/pdm-api-types/src/sdn.rs
index a291c11..34ab36c 100644
--- a/lib/pdm-api-types/src/sdn.rs
+++ b/lib/pdm-api-types/src/sdn.rs
@@ -1,5 +1,5 @@
-use proxmox_schema::{api, const_regex, ApiStringFormat, IntegerSchema, Schema, StringSchema};
-use pve_api_types::{SdnVnet, SdnZone};
+use proxmox_schema::{api, ApiStringFormat, IntegerSchema, Schema, StringSchema};
+use pve_api_types::{SdnController, SdnVnet, SdnZone};
 use serde::{Deserialize, Serialize};
 
 use crate::remotes::REMOTE_ID_SCHEMA;
@@ -113,6 +113,25 @@ pub struct CreateVnetParams {
     pub remotes: Vec<CreateVnetRemote>,
 }
 
+#[api(
+    properties: {
+        remote: {
+            schema: REMOTE_ID_SCHEMA,
+        },
+        controller: {
+            type: pve_api_types::SdnController,
+            flatten: true,
+        }
+    }
+)]
+/// SDN controller with additional information about which remote it belongs to
+#[derive(Serialize, Deserialize, Clone, PartialEq)]
+pub struct ListController {
+    pub remote: String,
+    #[serde(flatten)]
+    pub controller: SdnController,
+}
+
 #[api(
     properties: {
         remote: {
diff --git a/lib/pdm-client/src/lib.rs b/lib/pdm-client/src/lib.rs
index a17a045..9314559 100644
--- a/lib/pdm-client/src/lib.rs
+++ b/lib/pdm-client/src/lib.rs
@@ -59,8 +59,10 @@ pub mod types {
 
     pub use pve_api_types::PveUpid;
 
-    pub use pdm_api_types::sdn::{CreateVnetParams, CreateZoneParams, ListVnet, ListZone};
-    pub use pve_api_types::ListZonesType;
+    pub use pdm_api_types::sdn::{
+        CreateVnetParams, CreateZoneParams, ListController, ListVnet, ListZone,
+    };
+    pub use pve_api_types::{ListControllersType, ListZonesType, SdnObjectState};
 }
 
 pub struct PdmClient<T: HttpApiClient>(pub T);
@@ -971,6 +973,21 @@ impl<T: HttpApiClient> PdmClient<T> {
             .data)
     }
 
+    pub async fn pve_sdn_list_controllers(
+        &self,
+        pending: impl Into<Option<bool>>,
+        running: impl Into<Option<bool>>,
+        ty: impl Into<Option<ListControllersType>>,
+    ) -> Result<Vec<ListController>, Error> {
+        let path = ApiPathBuilder::new("/api2/extjs/sdn/controllers".to_string())
+            .maybe_arg("pending", &pending.into())
+            .maybe_arg("running", &running.into())
+            .maybe_arg("ty", &ty.into())
+            .build();
+
+        Ok(self.0.get(&path).await?.expect_json()?.data)
+    }
+
     pub async fn pve_sdn_list_zones(
         &self,
         pending: impl Into<Option<bool>>,
diff --git a/server/src/api/sdn/controllers.rs b/server/src/api/sdn/controllers.rs
new file mode 100644
index 0000000..271397a
--- /dev/null
+++ b/server/src/api/sdn/controllers.rs
@@ -0,0 +1,114 @@
+use std::collections::HashSet;
+
+use anyhow::Error;
+
+use pbs_api_types::REMOTE_ID_SCHEMA;
+use pdm_api_types::{remotes::RemoteType, sdn::ListController};
+use proxmox_router::Router;
+use proxmox_schema::api;
+use pve_api_types::ListControllersType;
+
+use crate::{
+    api::pve,
+    parallel_fetcher::{NodeResults, ParallelFetcher},
+};
+
+pub const ROUTER: Router = Router::new().get(&API_METHOD_LIST_CONTROLLERS);
+
+#[api(
+    input: {
+        properties: {
+            pending: {
+                type: Boolean,
+                optional: true,
+                description: "Include a list of attributes whose changes are currently pending.",
+            },
+            running: {
+                type: Boolean,
+                optional: true,
+                description: "If true shows the running configuration, otherwise the pending configuration.",
+            },
+            ty: {
+                type: ListControllersType,
+                optional: true,
+            },
+            remotes: {
+                type: Array,
+                optional: true,
+                description: "Only return controllers from the specified remotes.",
+                items: {
+                    schema: REMOTE_ID_SCHEMA,
+                }
+            },
+        }
+    },
+    returns: {
+        type: Array,
+        description: "Get a list of controllers fitting the filtering criteria.",
+        items: {
+            type: ListController,
+        },
+    },
+)]
+/// Query controllers of remotes with optional filtering options
+pub async fn list_controllers(
+    pending: Option<bool>,
+    running: Option<bool>,
+    ty: Option<ListControllersType>,
+    remotes: Option<HashSet<String>>,
+) -> Result<Vec<ListController>, Error> {
+    let (remote_config, _) = pdm_config::remotes::config()?;
+
+    let filtered_remotes = remote_config.into_iter().filter_map(|(_, remote)| {
+        if remote.ty == RemoteType::Pve
+            && remotes
+                .as_ref()
+                .map(|remotes| remotes.contains(&remote.id))
+                .unwrap_or(true)
+        {
+            return Some(remote);
+        }
+
+        None
+    });
+
+    let mut vnets = Vec::new();
+    let fetcher = ParallelFetcher::new((pending, running, ty));
+
+    let results = fetcher
+        .do_for_all_remotes(filtered_remotes, async |ctx, r, _| {
+            Ok(pve::connect(&r)?
+                .list_controllers(ctx.0, ctx.1, ctx.2)
+                .await?)
+        })
+        .await;
+
+    for (remote, remote_result) in results.remote_results.into_iter() {
+        match remote_result {
+            Ok(remote_result) => {
+                for (node, node_result) in remote_result.node_results.into_iter() {
+                    match node_result {
+                        Ok(NodeResults { data, .. }) => {
+                            vnets.extend(data.into_iter().map(|controller| ListController {
+                                remote: remote.clone(),
+                                controller,
+                            }))
+                        }
+                        Err(error) => {
+                            log::error!(
+                                "could not fetch vnets from remote {} node {}: {error:#}",
+                                remote,
+                                node
+                            );
+                        }
+                    }
+                }
+            }
+            Err(error) => {
+                log::error!("could not fetch vnets from remote {}: {error:#}", remote)
+            }
+        }
+    }
+
+    Ok(vnets)
+}
diff --git a/server/src/api/sdn/mod.rs b/server/src/api/sdn/mod.rs
index ccf7123..ef0f8b9 100644
--- a/server/src/api/sdn/mod.rs
+++ b/server/src/api/sdn/mod.rs
@@ -1,11 +1,13 @@
 use proxmox_router::{list_subdirs_api_method, Router, SubdirMap};
 use proxmox_sortable_macro::sortable;
 
+pub mod controllers;
 pub mod vnets;
 pub mod zones;
 
 #[sortable]
 pub const SUBDIRS: SubdirMap = &sorted!([
+    ("controllers", &controllers::ROUTER),
     ("vnets", &vnets::ROUTER),
     ("zones", &zones::ROUTER),
 ]);
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v4 08/15] ui: sdn: add EvpnRouteTarget type
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (21 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 07/15] api: sdn: add list_controllers endpoint Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 09/15] ui: sdn: add vnet icon Stefan Hanreich
                   ` (9 subsequent siblings)
  32 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

A helper type that can be used to parse the route targets contained in
the EVPN zones 'Route Target Import' API call.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 ui/src/sdn/evpn/mod.rs | 26 ++++++++++++++++++++++++++
 1 file changed, 26 insertions(+)
 create mode 100644 ui/src/sdn/evpn/mod.rs

diff --git a/ui/src/sdn/evpn/mod.rs b/ui/src/sdn/evpn/mod.rs
new file mode 100644
index 0000000..2515354
--- /dev/null
+++ b/ui/src/sdn/evpn/mod.rs
@@ -0,0 +1,26 @@
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)]
+pub struct EvpnRouteTarget {
+    asn: u32,
+    vni: u32,
+}
+
+impl std::str::FromStr for EvpnRouteTarget {
+    type Err = anyhow::Error;
+
+    fn from_str(value: &str) -> Result<Self, Self::Err> {
+        if let Some((asn, vni)) = value.split_once(':') {
+            return Ok(Self {
+                asn: asn.parse()?,
+                vni: vni.parse()?,
+            });
+        }
+
+        anyhow::bail!("could not parse EVPN route target!")
+    }
+}
+
+impl std::fmt::Display for EvpnRouteTarget {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
+        write!(f, "{}:{}", self.asn, self.vni)
+    }
+}
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v4 09/15] ui: sdn: add vnet icon
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (22 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 08/15] ui: sdn: add EvpnRouteTarget type Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 10/15] ui: sdn: add view for showing evpn zones Stefan Hanreich
                   ` (8 subsequent siblings)
  32 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

The icon for VNets is not contained in font awesome, so we add it
manually here for using it in PDM.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 ui/css/pdm.scss             | 14 +++++++++++++-
 ui/images/icon-sdn-vnet.svg |  6 ++++++
 2 files changed, 19 insertions(+), 1 deletion(-)
 create mode 100644 ui/images/icon-sdn-vnet.svg

diff --git a/ui/css/pdm.scss b/ui/css/pdm.scss
index 8e6fbb3..f0940a6 100644
--- a/ui/css/pdm.scss
+++ b/ui/css/pdm.scss
@@ -59,9 +59,21 @@
     display: inline-block;
 }
 
+.fa-sdn-vnet::before {
+    content: " ";
+    background-image: url(./images/icon-sdn-vnet.svg);
+    background-size: 16px 16px;
+    background-repeat: no-repeat;
+    width: 16px;
+    height: 16px;
+    vertical-align: bottom;
+    display: inline-block;
+}
+
 :root.pwt-dark-mode {
     .fa-memory,
-    .fa-cpu {
+    .fa-cpu,
+    .fa-sdn-vnet {
         filter: invert(90%);
     }
 }
diff --git a/ui/images/icon-sdn-vnet.svg b/ui/images/icon-sdn-vnet.svg
new file mode 100644
index 0000000..9d8becf
--- /dev/null
+++ b/ui/images/icon-sdn-vnet.svg
@@ -0,0 +1,6 @@
+<!-- from open parts of font-awesome 5
+https://github.com/FortAwesome/Font-Awesome/blob/ce084cb3463f15fd6b001eb70622d00a0e43c56c/svgs/solid/network-wired.svg
+-->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512">
+    <path d="M640 264v-16c0-8.84-7.16-16-16-16H344v-40h72c17.67 0 32-14.33 32-32V32c0-17.67-14.33-32-32-32H224c-17.67 0-32 14.33-32 32v128c0 17.67 14.33 32 32 32h72v40H16c-8.84 0-16 7.16-16 16v16c0 8.84 7.16 16 16 16h104v40H64c-17.67 0-32 14.33-32 32v128c0 17.67 14.33 32 32 32h160c17.67 0 32-14.33 32-32V352c0-17.67-14.33-32-32-32h-56v-40h304v40h-56c-17.67 0-32 14.33-32 32v128c0 17.67 14.33 32 32 32h160c17.67 0 32-14.33 32-32V352c0-17.67-14.33-32-32-32h-56v-40h104c8.84 0 16-7.16 16-16zM256 128V64h128v64H256zm-64 320H96v-64h96v64zm352 0h-96v-64h96v64z"/>
+</svg>
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v4 10/15] ui: sdn: add view for showing evpn zones
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (23 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 09/15] ui: sdn: add vnet icon Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 11/15] ui: sdn: add view for showing ip vrfs Stefan Hanreich
                   ` (7 subsequent siblings)
  32 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

This component shows the current state of all EVPN zones across all
remotes. It shows which zones are configured on a remote and which
VNets it contains. It also shows which remote VNets get imported via
the 'Import Route Target' setting in the EVPN zone. While the ZoneTree
component shows the contents of an IP-VRF merged across all remotes,
the RemoteTree shows the contents of a specific zone on a specific
remote, including all imported VNets.

Similar to the ZoneTree component, this component operates under the
assumption that ASNs are not reused across different remotes, unless
those zones are actually interconnected. For example: One zone imports
a specific ASN:VNI route target. If two zones with the same ASN:VNI
combination exist, but only one of them is connected to the zone, it
will still show the VNets of *both* zones as imported, since it merges
based on ASN:VNI.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 ui/src/sdn/evpn/mod.rs         |   3 +
 ui/src/sdn/evpn/remote_tree.rs | 496 +++++++++++++++++++++++++++++++++
 2 files changed, 499 insertions(+)
 create mode 100644 ui/src/sdn/evpn/remote_tree.rs

diff --git a/ui/src/sdn/evpn/mod.rs b/ui/src/sdn/evpn/mod.rs
index 2515354..c2958f0 100644
--- a/ui/src/sdn/evpn/mod.rs
+++ b/ui/src/sdn/evpn/mod.rs
@@ -1,3 +1,6 @@
+mod remote_tree;
+pub use remote_tree::RemoteTree;
+
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)]
 pub struct EvpnRouteTarget {
     asn: u32,
diff --git a/ui/src/sdn/evpn/remote_tree.rs b/ui/src/sdn/evpn/remote_tree.rs
new file mode 100644
index 0000000..746e69a
--- /dev/null
+++ b/ui/src/sdn/evpn/remote_tree.rs
@@ -0,0 +1,496 @@
+use std::cmp::Ordering;
+use std::collections::HashSet;
+use std::rc::Rc;
+use std::str::FromStr;
+
+use anyhow::{anyhow, Error};
+use pwt::widget::{error_message, Column};
+use yew::virtual_dom::{Key, VNode};
+use yew::{Component, Context, Html, Properties};
+
+use pdm_client::types::{ListController, ListVnet, ListZone, SdnObjectState};
+use pwt::css;
+use pwt::props::{ContainerBuilder, ExtractPrimaryKey, WidgetBuilder};
+use pwt::state::{Selection, SlabTree, TreeStore};
+use pwt::tr;
+use pwt::widget::data_table::{
+    DataTable, DataTableColumn, DataTableHeader, DataTableRowRenderArgs,
+};
+use pwt::widget::{Fa, Row};
+use pwt_macros::widget;
+
+use crate::sdn::evpn::EvpnRouteTarget;
+
+#[widget(comp=RemoteTreeComponent)]
+#[derive(Clone, PartialEq, Properties, Default)]
+pub struct RemoteTree {
+    zones: Rc<Vec<ListZone>>,
+    vnets: Rc<Vec<ListVnet>>,
+    controllers: Rc<Vec<ListController>>,
+}
+
+impl RemoteTree {
+    pub fn new(
+        zones: Rc<Vec<ListZone>>,
+        vnets: Rc<Vec<ListVnet>>,
+        controllers: Rc<Vec<ListController>>,
+    ) -> Self {
+        yew::props!(Self {
+            zones,
+            vnets,
+            controllers,
+        })
+    }
+}
+
+#[derive(Clone, PartialEq, Debug)]
+struct RemoteData {
+    id: String,
+    asn: u32,
+}
+
+#[derive(Clone, PartialEq, Debug)]
+struct ZoneData {
+    id: String,
+    remote: String,
+    route_target: EvpnRouteTarget,
+    import_targets: HashSet<EvpnRouteTarget>,
+    state: Option<SdnObjectState>,
+    controller_id: String,
+}
+
+#[derive(Clone, PartialEq, Debug)]
+struct VnetData {
+    parent_remote: String,
+    parent_zone: String,
+    id: String,
+    zone: String,
+    remote: String,
+    route_target: EvpnRouteTarget,
+    imported: bool,
+    external: bool,
+    state: Option<SdnObjectState>,
+}
+
+#[derive(Clone, PartialEq, Debug)]
+enum RemoteTreeEntry {
+    Root,
+    Remote(RemoteData),
+    Zone(ZoneData),
+    Vnet(VnetData),
+}
+
+impl ExtractPrimaryKey for RemoteTreeEntry {
+    fn extract_key(&self) -> Key {
+        match self {
+            Self::Root => Key::from("root"),
+            Self::Remote(remote) => Key::from(remote.id.clone()),
+            Self::Zone(zone) => Key::from(format!("{}/{}", zone.remote, zone.id)),
+            Self::Vnet(vnet) => Key::from(format!(
+                "{}/{}/{}/{}/{}",
+                vnet.remote, vnet.parent_remote, vnet.parent_zone, vnet.zone, vnet.id
+            )),
+        }
+    }
+}
+
+impl RemoteTreeEntry {
+    fn name(&self) -> Option<String> {
+        match self {
+            RemoteTreeEntry::Root => None,
+            RemoteTreeEntry::Remote(remote) => {
+                Some(format!("{} (ASN: {})", &remote.id, remote.asn))
+            }
+            RemoteTreeEntry::Zone(zone) => Some(zone.id.to_string()),
+            RemoteTreeEntry::Vnet(vnet) => Some(vnet.id.to_string()),
+        }
+    }
+
+    fn remote(&self) -> Option<&str> {
+        match self {
+            RemoteTreeEntry::Root => None,
+            RemoteTreeEntry::Remote(remote) => Some(&remote.id),
+            RemoteTreeEntry::Zone(zone) => Some(&zone.remote),
+            RemoteTreeEntry::Vnet(vnet) => Some(&vnet.remote),
+        }
+    }
+
+    fn l2vni(&self) -> Option<u32> {
+        match self {
+            RemoteTreeEntry::Vnet(vnet) => Some(vnet.route_target.vni),
+            _ => None,
+        }
+    }
+
+    fn l3vni(&self) -> Option<u32> {
+        match self {
+            RemoteTreeEntry::Zone(zone) => Some(zone.route_target.vni),
+            _ => None,
+        }
+    }
+
+    fn external(&self) -> Option<bool> {
+        match self {
+            RemoteTreeEntry::Vnet(vnet) => Some(vnet.external),
+            _ => None,
+        }
+    }
+
+    fn imported(&self) -> Option<bool> {
+        match self {
+            RemoteTreeEntry::Vnet(vnet) => Some(vnet.imported),
+            _ => None,
+        }
+    }
+}
+
+fn zones_to_remote_view(
+    controllers: &[ListController],
+    zones: &[ListZone],
+    vnets: &[ListVnet],
+) -> Result<SlabTree<RemoteTreeEntry>, Error> {
+    let mut tree = SlabTree::new();
+
+    let mut root = tree.set_root(RemoteTreeEntry::Root);
+    root.set_expanded(true);
+
+    for zone in zones {
+        let zone_data = &zone.zone;
+
+        let zone_controller_id = zone_data.controller.as_ref().ok_or_else(|| {
+            anyhow!(tr!(
+                "EVPN zone {} has no controller defined!",
+                zone_data.zone
+            ))
+        })?;
+
+        let controller = controllers
+            .iter()
+            .find(|controller| {
+                controller.remote == zone.remote
+                    && zone_controller_id == &controller.controller.controller
+            })
+            .ok_or_else(|| {
+                anyhow!(tr!(
+                    "Could not find Controller for EVPN zone {}",
+                    zone_data.zone
+                ))
+            })?;
+
+        let route_target = EvpnRouteTarget {
+            asn: controller.controller.asn.ok_or_else(|| {
+                anyhow!(tr!(
+                    "EVPN controller {} has no ASN defined!",
+                    controller.controller.controller
+                ))
+            })?,
+            vni: zone.zone.vrf_vxlan.ok_or_else(|| {
+                anyhow!(tr!("EVPN Zone {} has no VXLAN ID defined!", zone_data.zone))
+            })?,
+        };
+
+        let import_targets = zone_data
+            .rt_import
+            .iter()
+            .flat_map(|rt_import| rt_import.split(',').map(EvpnRouteTarget::from_str))
+            .collect::<Result<_, Error>>()?;
+
+        let remote_entry = root.children_mut().find(|remote_entry| {
+            if let RemoteTreeEntry::Remote(remote) = remote_entry.record() {
+                return remote.id == zone.remote;
+            }
+
+            false
+        });
+
+        let zone_entry = RemoteTreeEntry::Zone(ZoneData {
+            id: zone_data.zone.clone(),
+            remote: zone.remote.clone(),
+            route_target,
+            import_targets,
+            state: zone_data.state,
+            controller_id: controller.controller.controller.clone(),
+        });
+
+        if let Some(mut remote_entry) = remote_entry {
+            remote_entry.append(zone_entry);
+        } else {
+            let mut new_remote_entry = root.append(RemoteTreeEntry::Remote(RemoteData {
+                id: zone.remote.clone(),
+                asn: route_target.asn,
+            }));
+
+            new_remote_entry.set_expanded(true);
+            new_remote_entry.append(zone_entry);
+        };
+    }
+
+    for vnet in vnets {
+        let vnet_data = &vnet.vnet;
+
+        let vnet_zone_id = vnet_data
+            .zone
+            .as_ref()
+            .ok_or_else(|| anyhow!(tr!("VNet {} has no zone defined!", vnet_data.vnet)))?;
+
+        let Some(zone) = zones
+            .iter()
+            .find(|zone| {
+                zone.remote == vnet.remote
+                    && vnet_zone_id == &zone.zone.zone
+        }) else {
+            // this VNet is not part of an EVPN zone, skip it
+            continue;
+        };
+
+        let zone_controller_id = zone.zone.controller.as_ref().ok_or_else(|| {
+            anyhow!(tr!(
+                "EVPN zone {} has no controller defined!",
+                &zone.zone.zone
+            ))
+        })?;
+
+        let controller = controllers
+            .iter()
+            .find(|controller| {
+                controller.remote == zone.remote
+                    && zone_controller_id == &controller.controller.controller
+            })
+            .ok_or_else(|| {
+                anyhow!(tr!(
+                    "Controller of EVPN zone {} does not exist",
+                    zone.zone.zone
+                ))
+            })?;
+
+        let controller_asn = controller.controller.asn.ok_or_else(|| {
+            anyhow!(tr!(
+                "EVPN controller {} has no ASN defined!",
+                controller.controller.controller
+            ))
+        })?;
+
+        let zone_target = EvpnRouteTarget {
+            asn: controller_asn,
+            vni: zone
+                .zone
+                .vrf_vxlan
+                .ok_or_else(|| anyhow!(tr!("EVPN Zone {} has no VRF VNI", zone.zone.zone)))?,
+        };
+
+        let vnet_target = EvpnRouteTarget {
+            asn: controller_asn,
+            vni: vnet_data
+                .tag
+                .ok_or_else(|| anyhow!(tr!("VNet {} has no VNI", vnet_data.vnet)))?,
+        };
+
+        for mut remote_entry in root.children_mut() {
+            for mut zone_entry in remote_entry.children_mut() {
+                if let RemoteTreeEntry::Zone(zone) = zone_entry.record() {
+                    let imported = if zone.route_target == zone_target {
+                        false
+                    } else if zone.import_targets.contains(&zone_target)
+                        || zone.import_targets.contains(&vnet_target)
+                    {
+                        true
+                    } else {
+                        continue;
+                    };
+
+                    zone_entry.append(RemoteTreeEntry::Vnet(VnetData {
+                        id: vnet.vnet.vnet.clone(),
+                        remote: vnet.remote.clone(),
+                        zone: vnet.vnet.zone.clone().unwrap(),
+                        route_target: vnet_target,
+                        imported,
+                        external: zone.remote != vnet.remote,
+                        parent_remote: zone.remote.clone(),
+                        parent_zone: zone.id.clone(),
+                        state: vnet.vnet.state,
+                    }));
+                }
+            }
+        }
+    }
+
+    Ok(tree)
+}
+pub struct RemoteTreeComponent {
+    store: TreeStore<RemoteTreeEntry>,
+    selection: Selection,
+    error_msg: Option<String>,
+    columns: Rc<Vec<DataTableHeader<RemoteTreeEntry>>>,
+}
+
+fn name_remote_sorter(a: &RemoteTreeEntry, b: &RemoteTreeEntry) -> Ordering {
+    (a.name(), a.remote()).cmp(&(b.name(), b.remote()))
+}
+
+fn default_sorter(a: &RemoteTreeEntry, b: &RemoteTreeEntry) -> Ordering {
+    (
+        a.external(),
+        a.imported(),
+        a.remote(),
+        a.name(),
+        a.l3vni(),
+        a.l2vni(),
+    )
+        .cmp(&(
+            b.external(),
+            b.imported(),
+            b.remote(),
+            b.name(),
+            b.l3vni(),
+            b.l2vni(),
+        ))
+}
+
+impl RemoteTreeComponent {
+    fn columns(store: TreeStore<RemoteTreeEntry>) -> Rc<Vec<DataTableHeader<RemoteTreeEntry>>> {
+        Rc::new(vec![
+            DataTableColumn::new(tr!("Name"))
+                .tree_column(store)
+                .sorter(name_remote_sorter)
+                .render(|item: &RemoteTreeEntry| {
+                    let name = item.name();
+
+                    name.map(|name| {
+                        let mut row = Row::new().class(css::AlignItems::Baseline).gap(2);
+
+                        row = match item {
+                            RemoteTreeEntry::Remote(_) => row.with_child(Fa::new("server")),
+                            RemoteTreeEntry::Zone(_) => row.with_child(Fa::new("th")),
+                            _ => row,
+                        };
+
+                        row = row.with_child(name);
+
+                        Html::from(row)
+                    })
+                    .unwrap_or_default()
+                })
+                .flex(2)
+                .into(),
+            DataTableColumn::new(tr!("Remote"))
+                .get_property(|item: &RemoteTreeEntry| match item {
+                    RemoteTreeEntry::Zone(zone) => zone.remote.as_str(),
+                    RemoteTreeEntry::Vnet(vnet) => vnet.remote.as_str(),
+                    _ => "",
+                })
+                .flex(1)
+                .into(),
+            DataTableColumn::new(tr!("L3VNI"))
+                .render(|item: &RemoteTreeEntry| item.l3vni().map(VNode::from).unwrap_or_default())
+                .sorter(|a: &RemoteTreeEntry, b: &RemoteTreeEntry| a.l3vni().cmp(&b.l3vni()))
+                .flex(1)
+                .into(),
+            DataTableColumn::new(tr!("L2VNI"))
+                .render(|item: &RemoteTreeEntry| item.l2vni().map(VNode::from).unwrap_or_default())
+                .sorter(|a: &RemoteTreeEntry, b: &RemoteTreeEntry| a.l2vni().cmp(&b.l2vni()))
+                .flex(1)
+                .into(),
+            DataTableColumn::new(tr!("External"))
+                .get_property_owned(|item: &RemoteTreeEntry| match item {
+                    RemoteTreeEntry::Vnet(vnet) if vnet.external => tr!("Yes"),
+                    RemoteTreeEntry::Vnet(vnet) if !vnet.external => tr!("No"),
+                    _ => String::new(),
+                })
+                .flex(1)
+                .into(),
+            DataTableColumn::new(tr!("Imported"))
+                .get_property_owned(|item: &RemoteTreeEntry| match item {
+                    RemoteTreeEntry::Vnet(vnet) if vnet.imported => tr!("Yes"),
+                    RemoteTreeEntry::Vnet(vnet) if !vnet.imported => tr!("No"),
+                    _ => String::new(),
+                })
+                .flex(1)
+                .into(),
+        ])
+    }
+}
+
+impl Component for RemoteTreeComponent {
+    type Properties = RemoteTree;
+    type Message = ();
+
+    fn create(ctx: &Context<Self>) -> Self {
+        let store = TreeStore::new().view_root(false);
+        let columns = Self::columns(store.clone());
+
+        let selection = Selection::new();
+        let mut error_msg = None;
+
+        match zones_to_remote_view(
+            &ctx.props().controllers,
+            &ctx.props().zones,
+            &ctx.props().vnets,
+        ) {
+            Ok(data) => {
+                store.set_data(data);
+                store.set_sorter(default_sorter);
+            }
+            Err(error) => {
+                error_msg = Some(error.to_string());
+            }
+        }
+
+        Self {
+            store,
+            selection,
+            columns,
+            error_msg,
+        }
+    }
+
+    fn view(&self, _ctx: &Context<Self>) -> Html {
+        let table = DataTable::new(self.columns.clone(), self.store.clone())
+            .striped(false)
+            .selection(self.selection.clone())
+            .row_render_callback(|args: &mut DataTableRowRenderArgs<RemoteTreeEntry>| {
+                match args.record() {
+                    RemoteTreeEntry::Vnet(vnet) if vnet.external || vnet.imported => {
+                        args.add_class("pwt-opacity-50");
+                    }
+                    RemoteTreeEntry::Remote(_) => args.add_class("pwt-bg-color-surface"),
+                    _ => (),
+                };
+            })
+            .class(css::FlexFit);
+
+        let mut column = Column::new().class(pwt::css::FlexFit).with_child(table);
+
+        if let Some(msg) = &self.error_msg {
+            column.add_child(error_message(msg.as_ref()));
+        }
+
+        column.into()
+    }
+
+    fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
+        if !Rc::ptr_eq(&ctx.props().zones, &old_props.zones)
+            || !Rc::ptr_eq(&ctx.props().vnets, &old_props.vnets)
+            || !Rc::ptr_eq(&ctx.props().controllers, &old_props.controllers)
+        {
+            match zones_to_remote_view(
+                &ctx.props().controllers,
+                &ctx.props().zones,
+                &ctx.props().vnets,
+            ) {
+                Ok(data) => {
+                    self.store.write().update_root_tree(data);
+                    self.store.set_sorter(default_sorter);
+
+                    self.error_msg = None;
+                }
+                Err(error) => {
+                    self.error_msg = Some(error.to_string());
+                }
+            }
+
+            return true;
+        }
+
+        false
+    }
+}
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v4 11/15] ui: sdn: add view for showing ip vrfs
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (24 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 10/15] ui: sdn: add view for showing evpn zones Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 12/15] ui: sdn: add component for creating evpn vnets Stefan Hanreich
                   ` (6 subsequent siblings)
  32 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

This component shows the content of all IP-VRFs from all remotes
configured in the PDM. It merges the contents of IP-VRFs that have the
same ASN:VNI (= Route Target) combination. Practically speaking, it
shows a list of VNets who would have the Route Target of the IP-VRF in
their EVPN route target. This means that when importing the IP-VRF in
a different zone, routes from guests in those VNets would be imported
into the routing table.

The component operates under the assumption that zones that are in the
same ASN, are also interconnected - since it merges the VNets of all
zones with the same route target (= ASN + VRF VNI). This means ASNs
cannot be reused across remotes, if they are not connected, in order
for this view to correctly show the contents of the IP-VRFs. In the
future this could be improved by storing IDs or tags on the PVE side,
reading them from PDM and then only merging the zones of remotes that
have the same ID / tag.

In addition to the terms zones / vnets, the terms IP-VRF and MAC-VRF
are introduced. For EVPN a zone maps to a routing table and a vnet
maps to a bridging table. FRR [1] uses the terms in their
documentation and they are also referred to as such in the EVPN RFC
[2]. In order to make this relationship more clear, particularly to
users that are familiar with EVPN but not necessarily Proxmox VE SDN,
those terms are now used in addition to the existing terms zone /
vnet.

[1] https://docs.frrouting.org/en/latest/evpn.html
[2] https://datatracker.ietf.org/doc/html/rfc8365

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 ui/src/lib.rs               |   2 +
 ui/src/sdn/evpn/mod.rs      |   3 +
 ui/src/sdn/evpn/vrf_tree.rs | 409 ++++++++++++++++++++++++++++++++++++
 ui/src/sdn/mod.rs           |   1 +
 4 files changed, 415 insertions(+)
 create mode 100644 ui/src/sdn/evpn/vrf_tree.rs
 create mode 100644 ui/src/sdn/mod.rs

diff --git a/ui/src/lib.rs b/ui/src/lib.rs
index e3755ec..bde8917 100644
--- a/ui/src/lib.rs
+++ b/ui/src/lib.rs
@@ -30,6 +30,8 @@ mod widget;
 pub mod pbs;
 pub mod pve;
 
+pub mod sdn;
+
 pub mod renderer;
 
 mod tasks;
diff --git a/ui/src/sdn/evpn/mod.rs b/ui/src/sdn/evpn/mod.rs
index c2958f0..da020a9 100644
--- a/ui/src/sdn/evpn/mod.rs
+++ b/ui/src/sdn/evpn/mod.rs
@@ -1,6 +1,9 @@
 mod remote_tree;
 pub use remote_tree::RemoteTree;
 
+mod vrf_tree;
+pub use vrf_tree::VrfTree;
+
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)]
 pub struct EvpnRouteTarget {
     asn: u32,
diff --git a/ui/src/sdn/evpn/vrf_tree.rs b/ui/src/sdn/evpn/vrf_tree.rs
new file mode 100644
index 0000000..8984891
--- /dev/null
+++ b/ui/src/sdn/evpn/vrf_tree.rs
@@ -0,0 +1,409 @@
+use std::cmp::Ordering;
+use std::collections::HashSet;
+use std::rc::Rc;
+
+use anyhow::{anyhow, Error};
+use yew::virtual_dom::{Key, VNode};
+use yew::{Component, Context, Html, Properties};
+
+use pdm_client::types::{ListController, ListVnet, ListZone};
+use pwt::css;
+use pwt::props::{ContainerBuilder, ExtractPrimaryKey, WidgetBuilder};
+use pwt::state::{Selection, SlabTree, TreeStore};
+use pwt::tr;
+use pwt::widget::data_table::{
+    DataTable, DataTableColumn, DataTableHeader, DataTableRowRenderArgs,
+};
+use pwt::widget::{error_message, Column, Fa, Row};
+use pwt_macros::widget;
+
+use crate::sdn::evpn::EvpnRouteTarget;
+
+#[widget(comp=VrfTreeComponent)]
+#[derive(Clone, PartialEq, Properties, Default)]
+pub struct VrfTree {
+    zones: Rc<Vec<ListZone>>,
+    vnets: Rc<Vec<ListVnet>>,
+    controllers: Rc<Vec<ListController>>,
+}
+
+impl VrfTree {
+    pub fn new(
+        zones: Rc<Vec<ListZone>>,
+        vnets: Rc<Vec<ListVnet>>,
+        controllers: Rc<Vec<ListController>>,
+    ) -> Self {
+        yew::props!(Self {
+            zones,
+            vnets,
+            controllers,
+        })
+    }
+}
+
+#[derive(Clone, PartialEq, Debug)]
+struct VrfData {
+    route_target: EvpnRouteTarget,
+}
+
+#[derive(Clone, PartialEq, Debug)]
+struct FdbData {
+    vrf_route_target: EvpnRouteTarget,
+    route_target: EvpnRouteTarget,
+}
+
+#[derive(Clone, PartialEq, Debug)]
+struct RemoteData {
+    remote: String,
+    zone: String,
+    vnet: String,
+}
+
+#[derive(Clone, PartialEq, Debug)]
+enum VrfTreeEntry {
+    Root,
+    Vrf(VrfData),
+    Fdb(FdbData),
+    Remote(RemoteData),
+}
+
+impl VrfTreeEntry {
+    fn vni(&self) -> Option<u32> {
+        match self {
+            VrfTreeEntry::Vrf(vrf) => Some(vrf.route_target.vni),
+            VrfTreeEntry::Fdb(fdb) => Some(fdb.route_target.vni),
+            _ => None,
+        }
+    }
+
+    fn asn(&self) -> Option<u32> {
+        match self {
+            VrfTreeEntry::Vrf(vrf) => Some(vrf.route_target.asn),
+            _ => None,
+        }
+    }
+
+    fn heading(&self) -> Option<String> {
+        Some(match self {
+            VrfTreeEntry::Root => return None,
+            VrfTreeEntry::Vrf(_) => "IP-VRF".to_string(),
+            VrfTreeEntry::Fdb(_) => "VNet".to_string(),
+            VrfTreeEntry::Remote(remote) => remote.vnet.clone(),
+        })
+    }
+}
+
+impl ExtractPrimaryKey for VrfTreeEntry {
+    fn extract_key(&self) -> Key {
+        match self {
+            Self::Root => Key::from("root"),
+            Self::Vrf(vrf) => Key::from(vrf.route_target.to_string()),
+            Self::Fdb(fdb) => Key::from(format!("{}/{}", fdb.vrf_route_target, fdb.route_target)),
+            Self::Remote(remote) => {
+                Key::from(format!("{}/{}/{}", remote.remote, remote.zone, remote.vnet,))
+            }
+        }
+    }
+}
+
+fn zones_to_vrf_view(
+    controllers: &[ListController],
+    zones: &[ListZone],
+    vnets: &[ListVnet],
+) -> Result<SlabTree<VrfTreeEntry>, Error> {
+    let mut tree = SlabTree::new();
+
+    let mut root = tree.set_root(VrfTreeEntry::Root);
+    root.set_expanded(true);
+
+    let mut existing_vrfs: HashSet<EvpnRouteTarget> = HashSet::new();
+
+    for zone in zones {
+        let zone_data = &zone.zone;
+
+        let zone_controller_id = zone_data.controller.as_ref().ok_or_else(|| {
+            anyhow!(tr!(
+                "EVPN zone {} has no controller defined!",
+                &zone_data.zone
+            ))
+        })?;
+
+        let controller = controllers
+            .iter()
+            .find(|controller| {
+                controller.remote == zone.remote
+                    && zone_controller_id == &controller.controller.controller
+            })
+            .ok_or_else(|| {
+                anyhow!(tr!(
+                    "Controller of EVPN zone {} does not exist",
+                    zone_data.zone
+                ))
+            })?;
+
+        let controller_asn = controller.controller.asn.ok_or_else(|| {
+            anyhow!(tr!(
+                "EVPN controller {} has no ASN defined!",
+                controller.controller.controller
+            ))
+        })?;
+
+        let route_target = EvpnRouteTarget {
+            asn: controller_asn,
+            vni: zone
+                .zone
+                .vrf_vxlan
+                .ok_or_else(|| anyhow!(tr!("EVPN Zone {} has no VRF VNI", zone_data.zone)))?,
+        };
+
+        if !existing_vrfs.insert(route_target) {
+            continue;
+        }
+
+        let mut vrf_entry = root.append(VrfTreeEntry::Vrf(VrfData { route_target }));
+        vrf_entry.set_expanded(true);
+    }
+
+    for vnet in vnets {
+        let vnet_data = &vnet.vnet;
+
+        let vnet_zone_id = vnet_data
+            .zone
+            .as_ref()
+            .ok_or_else(|| anyhow!(tr!("VNet {} has no zone defined!", vnet_data.vnet)))?;
+
+        let Some(zone) = zones
+            .iter()
+            .find(|zone| {
+                zone.remote == vnet.remote
+                    && vnet_zone_id == &zone.zone.zone
+        }) else {
+            // this VNet is not part of an EVPN zone, skip it
+            continue;
+        };
+
+        let zone_controller_id = zone.zone.controller.as_ref().ok_or_else(|| {
+            anyhow!(tr!(
+                "EVPN zone {} has no controller defined!",
+                &zone.zone.zone
+            ))
+        })?;
+
+        let controller = controllers
+            .iter()
+            .find(|controller| {
+                controller.remote == zone.remote
+                    && zone_controller_id == &controller.controller.controller
+            })
+            .ok_or_else(|| {
+                anyhow!(tr!(
+                    "Controller of EVPN zone {} does not exist",
+                    zone.zone.zone
+                ))
+            })?;
+
+        let controller_asn = controller.controller.asn.ok_or_else(|| {
+            anyhow!(tr!(
+                "EVPN controller {} has no ASN defined!",
+                controller.controller.controller
+            ))
+        })?;
+
+        let zone_target = EvpnRouteTarget {
+            asn: controller_asn,
+            vni: zone
+                .zone
+                .vrf_vxlan
+                .ok_or_else(|| anyhow!(tr!("EVPN Zone {} has no VRF VNI", zone.zone.zone)))?,
+        };
+
+        let vnet_target = EvpnRouteTarget {
+            asn: controller_asn,
+            vni: vnet_data
+                .tag
+                .ok_or_else(|| anyhow!(tr!("VNet {} has no VNI", vnet_data.vnet)))?,
+        };
+
+        for mut vrf_entry in root.children_mut() {
+            if let VrfTreeEntry::Vrf(vrf_data) = vrf_entry.record() {
+                if vrf_data.route_target != zone_target {
+                    continue;
+                }
+
+                let searched_entry = vrf_entry.children_mut().find(|entry| {
+                    if let VrfTreeEntry::Fdb(fdb_data) = entry.record() {
+                        return fdb_data.route_target == vnet_target;
+                    }
+
+                    false
+                });
+
+                let mut fdb_entry = if let Some(fdb_entry) = searched_entry {
+                    fdb_entry
+                } else {
+                    let fdb_entry = vrf_entry.append(VrfTreeEntry::Fdb(FdbData {
+                        vrf_route_target: zone_target,
+                        route_target: vnet_target,
+                    }));
+
+                    fdb_entry
+                };
+
+                let vnet_zone =
+                    vnet.vnet.zone.as_ref().ok_or_else(|| {
+                        anyhow!(tr!("VNet {} has no zone defined!", vnet.vnet.vnet))
+                    })?;
+
+                fdb_entry.append(VrfTreeEntry::Remote(RemoteData {
+                    remote: vnet.remote.clone(),
+                    zone: vnet_zone.clone(),
+                    vnet: vnet.vnet.vnet.clone(),
+                }));
+            }
+        }
+    }
+
+    Ok(tree)
+}
+
+pub struct VrfTreeComponent {
+    store: TreeStore<VrfTreeEntry>,
+    selection: Selection,
+    error_msg: Option<String>,
+    columns: Rc<Vec<DataTableHeader<VrfTreeEntry>>>,
+}
+
+fn default_sorter(a: &VrfTreeEntry, b: &VrfTreeEntry) -> Ordering {
+    (a.asn(), a.vni()).cmp(&(b.asn(), b.vni()))
+}
+
+impl VrfTreeComponent {
+    fn columns(store: TreeStore<VrfTreeEntry>) -> Rc<Vec<DataTableHeader<VrfTreeEntry>>> {
+        Rc::new(vec![
+            DataTableColumn::new(tr!("Type / Name"))
+                .tree_column(store)
+                .render(|item: &VrfTreeEntry| {
+                    let heading = item.heading();
+
+                    heading
+                        .map(|heading| {
+                            let mut row = Row::new().class(css::AlignItems::Baseline).gap(2);
+
+                            row = match item {
+                                VrfTreeEntry::Vrf(_) => row.with_child(Fa::new("th")),
+                                VrfTreeEntry::Fdb(_) => row.with_child(Fa::new("sdn-vnet")),
+                                _ => row,
+                            };
+
+                            row = row.with_child(heading);
+
+                            Html::from(row)
+                        })
+                        .unwrap_or_default()
+                })
+                .sorter(default_sorter)
+                .into(),
+            DataTableColumn::new(tr!("ASN"))
+                .render(|item: &VrfTreeEntry| item.asn().map(VNode::from).unwrap_or_default())
+                .sorter(|a: &VrfTreeEntry, b: &VrfTreeEntry| a.asn().cmp(&b.asn()))
+                .into(),
+            DataTableColumn::new(tr!("VNI"))
+                .render(|item: &VrfTreeEntry| item.vni().map(VNode::from).unwrap_or_default())
+                .sorter(|a: &VrfTreeEntry, b: &VrfTreeEntry| a.vni().cmp(&b.vni()))
+                .into(),
+            DataTableColumn::new(tr!("Zone"))
+                .get_property(|item: &VrfTreeEntry| match item {
+                    VrfTreeEntry::Remote(remote) => remote.zone.as_str(),
+                    _ => "",
+                })
+                .into(),
+            DataTableColumn::new(tr!("Remote"))
+                .get_property(|item: &VrfTreeEntry| match item {
+                    VrfTreeEntry::Remote(remote) => remote.remote.as_str(),
+                    _ => "",
+                })
+                .into(),
+        ])
+    }
+}
+
+impl Component for VrfTreeComponent {
+    type Properties = VrfTree;
+    type Message = ();
+
+    fn create(ctx: &Context<Self>) -> Self {
+        let store = TreeStore::new().view_root(false);
+        let columns = Self::columns(store.clone());
+
+        let selection = Selection::new();
+        let mut error_msg = None;
+
+        match zones_to_vrf_view(
+            &ctx.props().controllers,
+            &ctx.props().zones,
+            &ctx.props().vnets,
+        ) {
+            Ok(data) => {
+                store.set_data(data);
+                store.set_sorter(default_sorter);
+            }
+            Err(error) => {
+                error_msg = Some(error.to_string());
+            }
+        }
+
+        Self {
+            store,
+            selection,
+            columns,
+            error_msg,
+        }
+    }
+
+    fn view(&self, _ctx: &Context<Self>) -> Html {
+        let table = DataTable::new(self.columns.clone(), self.store.clone())
+            .striped(false)
+            .selection(self.selection.clone())
+            .row_render_callback(|args: &mut DataTableRowRenderArgs<VrfTreeEntry>| {
+                if let VrfTreeEntry::Vrf(_) = args.record() {
+                    args.add_class("pwt-bg-color-surface");
+                }
+            })
+            .class(css::FlexFit);
+
+        let mut column = Column::new().class(pwt::css::FlexFit).with_child(table);
+
+        if let Some(msg) = &self.error_msg {
+            column.add_child(error_message(msg.as_ref()));
+        }
+
+        column.into()
+    }
+
+    fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
+        if !Rc::ptr_eq(&ctx.props().zones, &old_props.zones)
+            || !Rc::ptr_eq(&ctx.props().vnets, &old_props.vnets)
+            || !Rc::ptr_eq(&ctx.props().controllers, &old_props.controllers)
+        {
+            match zones_to_vrf_view(
+                &ctx.props().controllers,
+                &ctx.props().zones,
+                &ctx.props().vnets,
+            ) {
+                Ok(data) => {
+                    self.store.write().update_root_tree(data);
+                    self.store.set_sorter(default_sorter);
+
+                    self.error_msg = None;
+                }
+                Err(error) => {
+                    self.error_msg = Some(error.to_string());
+                }
+            }
+
+            return true;
+        }
+
+        false
+    }
+}
diff --git a/ui/src/sdn/mod.rs b/ui/src/sdn/mod.rs
new file mode 100644
index 0000000..ef2eab9
--- /dev/null
+++ b/ui/src/sdn/mod.rs
@@ -0,0 +1 @@
+pub mod evpn;
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v4 12/15] ui: sdn: add component for creating evpn vnets
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (25 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 11/15] ui: sdn: add view for showing ip vrfs Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 13/15] ui: sdn: add component for creatin evpn zones Stefan Hanreich
                   ` (5 subsequent siblings)
  32 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

This windows shows a form containing all fields required to create new
VNet via the create_vnet API endpoint and will initially be used by
the EVPN panel.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 lib/pdm-client/src/lib.rs   |   2 +-
 ui/src/sdn/evpn/add_vnet.rs | 313 ++++++++++++++++++++++++++++++++++++
 ui/src/sdn/evpn/mod.rs      |   3 +
 3 files changed, 317 insertions(+), 1 deletion(-)
 create mode 100644 ui/src/sdn/evpn/add_vnet.rs

diff --git a/lib/pdm-client/src/lib.rs b/lib/pdm-client/src/lib.rs
index 9314559..4573271 100644
--- a/lib/pdm-client/src/lib.rs
+++ b/lib/pdm-client/src/lib.rs
@@ -60,7 +60,7 @@ pub mod types {
     pub use pve_api_types::PveUpid;
 
     pub use pdm_api_types::sdn::{
-        CreateVnetParams, CreateZoneParams, ListController, ListVnet, ListZone,
+        CreateVnetParams, CreateZoneParams, ListController, ListVnet, ListZone, SDN_ID_SCHEMA,
     };
     pub use pve_api_types::{ListControllersType, ListZonesType, SdnObjectState};
 }
diff --git a/ui/src/sdn/evpn/add_vnet.rs b/ui/src/sdn/evpn/add_vnet.rs
new file mode 100644
index 0000000..89b2deb
--- /dev/null
+++ b/ui/src/sdn/evpn/add_vnet.rs
@@ -0,0 +1,313 @@
+use std::{collections::HashSet, rc::Rc};
+
+use anyhow::{bail, format_err, Error};
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+use yew::{virtual_dom::Key, Callback, Component, Html, Properties};
+
+use pdm_client::types::{CreateVnetParams, ListZone, SDN_ID_SCHEMA};
+use proxmox_yew_comp::{EditWindow, SchemaValidation};
+use pwt::{
+    css,
+    props::{
+        ContainerBuilder, CssBorderBuilder, CssPaddingBuilder, ExtractPrimaryKey, FieldBuilder,
+        WidgetBuilder, WidgetStyleBuilder,
+    },
+    state::{Selection, Store},
+    tr,
+    widget::{
+        data_table::{DataTable, DataTableColumn, DataTableHeader, MultiSelectMode},
+        error_message,
+        form::{
+            Field, FormContext, ManagedField, ManagedFieldContext, ManagedFieldMaster,
+            ManagedFieldState, Number,
+        },
+        Column, Container, GridPicker, InputPanel,
+    },
+};
+use pwt_macros::widget;
+
+use crate::pdm_client;
+
+#[widget(comp=AddVnetWindowComp)]
+#[derive(Properties, PartialEq, Clone)]
+pub struct AddVnetWindow {
+    pub zones: Rc<Vec<ListZone>>,
+    pub on_success: Option<Callback<String>>,
+    pub on_close: Option<Callback<()>>,
+}
+
+impl AddVnetWindow {
+    pub fn new(
+        zones: Rc<Vec<ListZone>>,
+        on_success: impl Into<Option<Callback<String>>>,
+        on_close: impl Into<Option<Callback<()>>>,
+    ) -> Self {
+        yew::props!(Self {
+            zones,
+            on_success: on_success.into(),
+            on_close: on_close.into(),
+        })
+    }
+}
+
+pub struct AddVnetWindowComp {}
+
+impl Component for AddVnetWindowComp {
+    type Message = ();
+
+    type Properties = AddVnetWindow;
+
+    fn create(_ctx: &yew::Context<Self>) -> Self {
+        Self {}
+    }
+
+    fn view(&self, ctx: &yew::Context<Self>) -> Html {
+        let props = ctx.props().clone();
+
+        EditWindow::new(tr!("Add VNet"))
+            .renderer(move |form_ctx: &FormContext| {
+                InputPanel::new()
+                    .class(css::FlexFit)
+                    .padding(4)
+                    .width("auto")
+                    .with_field(
+                        tr!("VNet ID"),
+                        Field::new()
+                            .name("vnet")
+                            .schema(&SDN_ID_SCHEMA)
+                            .required(true),
+                    )
+                    .with_field(
+                        tr!("VXLAN VNI"),
+                        Number::<u32>::new()
+                            .min(1)
+                            .max(16777215)
+                            .name("tag")
+                            .required(true),
+                    )
+                    .with_custom_child(
+                        Column::new()
+                            .with_child(ZoneTable::new(props.zones.clone()).name("remotes"))
+                            .with_optional_child(
+                                form_ctx
+                                    .read()
+                                    .get_field_valid("remotes")
+                                    .and_then(|result| result.err().as_deref().map(error_message)),
+                            ),
+                    )
+                    .into()
+            })
+            .on_close(ctx.props().on_close.clone())
+            .on_submit({
+                let on_success = props.on_success.clone();
+
+                move |form_ctx: FormContext| {
+                    let on_success = on_success.clone();
+
+                    async move {
+                        let client = pdm_client();
+
+                        let params: CreateVnetParams =
+                            serde_json::from_value(form_ctx.get_submit_data()).unwrap();
+
+                        let upid = client.pve_sdn_create_vnet(params).await?;
+
+                        if let Some(cb) = on_success {
+                            cb.emit(upid)
+                        }
+
+                        Ok(())
+                    }
+                }
+            })
+            .into()
+    }
+}
+
+#[widget(comp=ManagedFieldMaster<ZoneTableComponent>, @input)]
+#[derive(Clone, PartialEq, Properties)]
+pub struct ZoneTable {
+    zones: Rc<Vec<ListZone>>,
+}
+
+impl ZoneTable {
+    pub fn new(zones: Rc<Vec<ListZone>>) -> Self {
+        yew::props!(Self { zones })
+    }
+}
+
+#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
+pub struct ZoneTableEntry {
+    remote: String,
+    zone: String,
+    vni: u32,
+}
+
+impl ExtractPrimaryKey for ZoneTableEntry {
+    fn extract_key(&self) -> Key {
+        Key::from(format!("{}/{}", self.remote, self.zone))
+    }
+}
+
+pub struct ZoneTableComponent {
+    store: Store<ZoneTableEntry>,
+    selection: Selection,
+    columns: Rc<Vec<DataTableHeader<ZoneTableEntry>>>,
+    error_msg: Option<String>,
+}
+
+pub enum ZoneTableMsg {
+    SelectionChange,
+}
+
+#[derive(PartialEq)]
+pub struct ValidationContext {
+    zone_count: usize,
+}
+
+impl ManagedField for ZoneTableComponent {
+    type Properties = ZoneTable;
+    type Message = ZoneTableMsg;
+    type ValidateClosure = ValidationContext;
+
+    fn validation_args(props: &Self::Properties) -> Self::ValidateClosure {
+        ValidationContext {
+            zone_count: props.zones.len(),
+        }
+    }
+
+    fn validator(props: &Self::ValidateClosure, value: &Value) -> Result<Value, Error> {
+        let selected_entries: Vec<ZoneTableEntry> = serde_json::from_value(value.clone())?;
+
+        if selected_entries.is_empty() {
+            if props.zone_count == 0 {
+                bail!(tr!("At least one zone needs to be configured on a remote"));
+            } else {
+                bail!(tr!("At least one zone needs to be selected"));
+            }
+        }
+
+        let mut unique = HashSet::new();
+
+        if !selected_entries
+            .iter()
+            .all(|entry| unique.insert(entry.remote.as_str()))
+        {
+            bail!(tr!("Can only create the VNet once per remote!"))
+        }
+
+        Ok(value.clone())
+    }
+
+    fn setup(_props: &Self::Properties) -> ManagedFieldState {
+        ManagedFieldState {
+            value: Value::Array(Vec::new()),
+            valid: Ok(()),
+            default: Value::Array(Vec::new()),
+            radio_group: false,
+            unique: false,
+        }
+    }
+
+    fn create(ctx: &ManagedFieldContext<Self>) -> Self {
+        let columns = Self::columns();
+        let link = ctx.link().clone();
+        let selection = Selection::new().multiselect(true).on_select(move |_| {
+            link.send_message(Self::Message::SelectionChange);
+        });
+
+        let store = Store::new();
+
+        let zones: Result<Vec<ZoneTableEntry>, Error> =
+            ctx.props()
+                .zones
+                .iter()
+                .map(|zone| {
+                    Ok(ZoneTableEntry {
+                        remote: zone.remote.clone(),
+                        zone: zone.zone.zone.clone(),
+                        vni: zone.zone.vrf_vxlan.ok_or_else(|| {
+                            format_err!(tr!("EVPN Zone has no VRF VNI configured!"))
+                        })?,
+                    })
+                })
+                .collect();
+
+        let mut error_msg = None;
+
+        match zones {
+            Ok(zones) => {
+                store.set_data(zones);
+            }
+            Err(error) => error_msg = Some(error.to_string()),
+        };
+
+        Self {
+            store,
+            selection,
+            columns,
+            error_msg,
+        }
+    }
+
+    fn update(&mut self, ctx: &ManagedFieldContext<Self>, msg: Self::Message) -> bool {
+        match msg {
+            Self::Message::SelectionChange => {
+                let read_guard = self.store.read();
+
+                ctx.link().update_value(
+                    serde_json::to_value(
+                        self.selection
+                            .selected_keys()
+                            .iter()
+                            .filter_map(|key| read_guard.lookup_record(key))
+                            .collect::<Vec<_>>(),
+                    )
+                    .unwrap(),
+                );
+            }
+        }
+
+        false
+    }
+
+    fn view(&self, _ctx: &ManagedFieldContext<Self>) -> Html {
+        let table = DataTable::new(self.columns.clone(), self.store.clone())
+            .multiselect_mode(MultiSelectMode::Simple)
+            .border(true)
+            .class(css::FlexFit);
+
+        let mut container =
+            Container::new().with_child(GridPicker::new(table).selection(self.selection.clone()));
+
+        if let Some(error_msg) = &self.error_msg {
+            container.add_child(error_message(error_msg));
+        }
+
+        container.into()
+    }
+}
+
+impl ZoneTableComponent {
+    fn columns() -> Rc<Vec<DataTableHeader<ZoneTableEntry>>> {
+        Rc::new(vec![
+            DataTableColumn::selection_indicator().into(),
+            DataTableColumn::new(tr!("Remote"))
+                .flex(1)
+                .render(move |item: &ZoneTableEntry| item.remote.as_str().into())
+                .sorter(|a: &ZoneTableEntry, b: &ZoneTableEntry| a.remote.cmp(&b.remote))
+                .into(),
+            DataTableColumn::new(tr!("Zone"))
+                .flex(1)
+                .render(move |item: &ZoneTableEntry| item.zone.as_str().into())
+                .sorter(|a: &ZoneTableEntry, b: &ZoneTableEntry| a.zone.cmp(&b.zone))
+                .into(),
+            DataTableColumn::new(tr!("VRF VNI"))
+                .flex(1)
+                .render(move |item: &ZoneTableEntry| item.vni.to_string().into())
+                .sorter(|a: &ZoneTableEntry, b: &ZoneTableEntry| a.vni.cmp(&b.vni))
+                .into(),
+        ])
+    }
+}
diff --git a/ui/src/sdn/evpn/mod.rs b/ui/src/sdn/evpn/mod.rs
index da020a9..9fda8a1 100644
--- a/ui/src/sdn/evpn/mod.rs
+++ b/ui/src/sdn/evpn/mod.rs
@@ -4,6 +4,9 @@ pub use remote_tree::RemoteTree;
 mod vrf_tree;
 pub use vrf_tree::VrfTree;
 
+mod add_vnet;
+pub use add_vnet::AddVnetWindow;
+
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)]
 pub struct EvpnRouteTarget {
     asn: u32,
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v4 13/15] ui: sdn: add component for creatin evpn zones
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (26 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 12/15] ui: sdn: add component for creating evpn vnets Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 14/15] ui: sdn: add evpn overview panel Stefan Hanreich
                   ` (4 subsequent siblings)
  32 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

Adds an edit window for creating a new zone. This windows shows a form
containing all fields required to create new zone via the create_zone
API endpoint and will initially be used by the EVPN panel.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 ui/src/sdn/evpn/add_zone.rs | 328 ++++++++++++++++++++++++++++++++++++
 ui/src/sdn/evpn/mod.rs      |   3 +
 2 files changed, 331 insertions(+)
 create mode 100644 ui/src/sdn/evpn/add_zone.rs

diff --git a/ui/src/sdn/evpn/add_zone.rs b/ui/src/sdn/evpn/add_zone.rs
new file mode 100644
index 0000000..ec4ae47
--- /dev/null
+++ b/ui/src/sdn/evpn/add_zone.rs
@@ -0,0 +1,328 @@
+use std::{collections::HashSet, rc::Rc};
+
+use anyhow::{bail, format_err, Error};
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+use yew::{virtual_dom::Key, Callback, Component, Html, Properties};
+
+use pdm_client::types::{CreateZoneParams, ListController, SDN_ID_SCHEMA};
+use proxmox_yew_comp::{EditWindow, SchemaValidation};
+use pwt::{
+    css,
+    props::{
+        ContainerBuilder, CssBorderBuilder, CssPaddingBuilder, ExtractPrimaryKey, FieldBuilder,
+        WidgetBuilder, WidgetStyleBuilder,
+    },
+    state::{Selection, Store},
+    tr,
+    widget::{
+        data_table::{DataTable, DataTableColumn, DataTableHeader, MultiSelectMode},
+        error_message,
+        form::{
+            Field, FormContext, ManagedField, ManagedFieldContext, ManagedFieldMaster,
+            ManagedFieldState, Number,
+        },
+        Column, Container, GridPicker, InputPanel,
+    },
+};
+use pwt_macros::widget;
+
+use crate::pdm_client;
+
+#[widget(comp=AddZoneWindowComp)]
+#[derive(Properties, PartialEq, Clone)]
+pub struct AddZoneWindow {
+    pub controllers: Rc<Vec<ListController>>,
+    pub on_success: Option<Callback<String>>,
+    pub on_close: Option<Callback<()>>,
+}
+
+impl AddZoneWindow {
+    pub fn new(
+        controllers: Rc<Vec<ListController>>,
+        on_success: impl Into<Option<Callback<String>>>,
+        on_close: impl Into<Option<Callback<()>>>,
+    ) -> Self {
+        yew::props!(Self {
+            controllers,
+            on_success: on_success.into(),
+            on_close: on_close.into(),
+        })
+    }
+}
+
+pub struct AddZoneWindowComp {}
+
+impl Component for AddZoneWindowComp {
+    type Message = ();
+
+    type Properties = AddZoneWindow;
+
+    fn create(_ctx: &yew::Context<Self>) -> Self {
+        Self {}
+    }
+
+    fn view(&self, ctx: &yew::Context<Self>) -> Html {
+        let props = ctx.props().clone();
+
+        EditWindow::new(tr!("Add Zone"))
+            .renderer(move |form_ctx: &FormContext| {
+                InputPanel::new()
+                    .class(css::FlexFit)
+                    .padding(4)
+                    .width("auto")
+                    .with_field(
+                        tr!("Zone ID"),
+                        Field::new()
+                            .name("zone")
+                            .schema(&SDN_ID_SCHEMA)
+                            .required(true),
+                    )
+                    .with_field(
+                        tr!("VRF VXLAN VNI"),
+                        Number::<u32>::new()
+                            .min(1)
+                            .max(16777215)
+                            .name("vrf-vxlan")
+                            .required(true),
+                    )
+                    .with_custom_child(
+                        Column::new()
+                            .with_child(
+                                ControllerTable::new(props.controllers.clone()).name("remotes"),
+                            )
+                            .with_optional_child(
+                                form_ctx
+                                    .read()
+                                    .get_field_valid("remotes")
+                                    .and_then(|result| result.err().as_deref().map(error_message)),
+                            ),
+                    )
+                    .into()
+            })
+            .on_close(ctx.props().on_close.clone())
+            .on_submit({
+                let on_success = props.on_success.clone();
+
+                move |form_ctx: FormContext| {
+                    let on_success = on_success.clone();
+
+                    async move {
+                        let client = pdm_client();
+
+                        let params: CreateZoneParams =
+                            serde_json::from_value(form_ctx.get_submit_data()).unwrap();
+
+                        let upid = client.pve_sdn_create_zone(params).await?;
+
+                        if let Some(cb) = on_success {
+                            cb.emit(upid)
+                        }
+
+                        Ok(())
+                    }
+                }
+            })
+            .into()
+    }
+}
+
+#[widget(comp=ManagedFieldMaster<ControllerTableComponent>, @input)]
+#[derive(Clone, PartialEq, Properties)]
+struct ControllerTable {
+    controllers: Rc<Vec<ListController>>,
+}
+
+impl ControllerTable {
+    pub fn new(controllers: Rc<Vec<ListController>>) -> Self {
+        yew::props!(Self { controllers })
+    }
+}
+
+#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
+struct ControllerTableEntry {
+    remote: String,
+    controller: String,
+    #[serde(skip)]
+    asn: u32,
+}
+
+impl ExtractPrimaryKey for ControllerTableEntry {
+    fn extract_key(&self) -> Key {
+        Key::from(format!("{}/{}", self.remote, self.controller))
+    }
+}
+
+struct ControllerTableComponent {
+    store: Store<ControllerTableEntry>,
+    selection: Selection,
+    columns: Rc<Vec<DataTableHeader<ControllerTableEntry>>>,
+    error_msg: Option<String>,
+}
+
+enum ControllerTableMsg {
+    SelectionChange,
+}
+
+#[derive(PartialEq)]
+struct ValidationContext {
+    controller_count: usize,
+}
+
+impl ManagedField for ControllerTableComponent {
+    type Properties = ControllerTable;
+    type Message = ControllerTableMsg;
+    type ValidateClosure = ValidationContext;
+
+    fn validation_args(props: &Self::Properties) -> Self::ValidateClosure {
+        ValidationContext {
+            controller_count: props.controllers.len(),
+        }
+    }
+
+    fn validator(props: &Self::ValidateClosure, value: &Value) -> Result<Value, Error> {
+        let selected_entries: Vec<ControllerTableEntry> = serde_json::from_value(value.clone())?;
+
+        if selected_entries.is_empty() {
+            if props.controller_count == 0 {
+                bail!(tr!(
+                    "at least one remote needs to have an EVPN controller configured"
+                ));
+            } else {
+                bail!(tr!("at least one EVPN controller needs to be selected"));
+            }
+        }
+
+        let mut unique = HashSet::new();
+
+        if !selected_entries
+            .iter()
+            .all(|entry| unique.insert(entry.remote.as_str()))
+        {
+            bail!(tr!("can only create the zone once per remote!"));
+        }
+
+        Ok(value.clone())
+    }
+
+    fn setup(_props: &Self::Properties) -> ManagedFieldState {
+        ManagedFieldState {
+            value: Value::Null,
+            valid: Ok(()),
+            default: Value::Array(Vec::new()),
+            radio_group: false,
+            unique: false,
+        }
+    }
+
+    fn create(ctx: &ManagedFieldContext<Self>) -> Self {
+        let link = ctx.link().clone();
+
+        let selection = Selection::new().multiselect(true).on_select(move |_| {
+            link.send_message(Self::Message::SelectionChange);
+        });
+
+        let store = Store::new();
+
+        let columns = Self::columns();
+
+        let controllers: Result<Vec<ControllerTableEntry>, Error> = ctx
+            .props()
+            .controllers
+            .iter()
+            .map(|controller| {
+                Ok(ControllerTableEntry {
+                    remote: controller.remote.clone(),
+                    controller: controller.controller.controller.clone(),
+                    asn: controller.controller.asn.ok_or_else(|| {
+                        format_err!(tr!(
+                            "EVPN controller {} has no ASN",
+                            controller.controller.controller
+                        ))
+                    })?,
+                })
+            })
+            .collect();
+
+        let mut error_msg = None;
+
+        match controllers {
+            Ok(controllers) => {
+                store.set_data(controllers);
+            }
+            Err(error) => error_msg = Some(error.to_string()),
+        };
+
+        Self {
+            store,
+            selection,
+            columns,
+            error_msg,
+        }
+    }
+
+    fn update(&mut self, ctx: &ManagedFieldContext<Self>, msg: Self::Message) -> bool {
+        match msg {
+            Self::Message::SelectionChange => {
+                let read_guard = self.store.read();
+
+                ctx.link().update_value(
+                    serde_json::to_value(
+                        self.selection
+                            .selected_keys()
+                            .iter()
+                            // todo: handle miss?
+                            .filter_map(|key| read_guard.lookup_record(key))
+                            .collect::<Vec<_>>(),
+                    )
+                    .unwrap(),
+                );
+            }
+        }
+
+        false
+    }
+
+    fn view(&self, _ctx: &ManagedFieldContext<Self>) -> Html {
+        let table = DataTable::new(self.columns.clone(), self.store.clone())
+            .multiselect_mode(MultiSelectMode::Simple)
+            .border(true)
+            .class(css::FlexFit);
+
+        let mut container =
+            Container::new().with_child(GridPicker::new(table).selection(self.selection.clone()));
+
+        if let Some(error_msg) = &self.error_msg {
+            container.add_child(error_message(error_msg));
+        }
+
+        container.into()
+    }
+}
+
+impl ControllerTableComponent {
+    fn columns() -> Rc<Vec<DataTableHeader<ControllerTableEntry>>> {
+        Rc::new(vec![
+            DataTableColumn::selection_indicator().into(),
+            DataTableColumn::new(tr!("Remote"))
+                .flex(1)
+                .render(move |item: &ControllerTableEntry| item.remote.as_str().into())
+                .sorter(|a: &ControllerTableEntry, b: &ControllerTableEntry| {
+                    a.remote.cmp(&b.remote)
+                })
+                .into(),
+            DataTableColumn::new(tr!("Controller"))
+                .flex(1)
+                .render(move |item: &ControllerTableEntry| item.controller.as_str().into())
+                .sorter(|a: &ControllerTableEntry, b: &ControllerTableEntry| {
+                    a.controller.cmp(&b.controller)
+                })
+                .into(),
+            DataTableColumn::new(tr!("ASN"))
+                .flex(1)
+                .render(move |item: &ControllerTableEntry| item.asn.into())
+                .sorter(|a: &ControllerTableEntry, b: &ControllerTableEntry| a.asn.cmp(&b.asn))
+                .into(),
+        ])
+    }
+}
diff --git a/ui/src/sdn/evpn/mod.rs b/ui/src/sdn/evpn/mod.rs
index 9fda8a1..adcf272 100644
--- a/ui/src/sdn/evpn/mod.rs
+++ b/ui/src/sdn/evpn/mod.rs
@@ -7,6 +7,9 @@ pub use vrf_tree::VrfTree;
 mod add_vnet;
 pub use add_vnet::AddVnetWindow;
 
+mod add_zone;
+pub use add_zone::AddZoneWindow;
+
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)]
 pub struct EvpnRouteTarget {
     asn: u32,
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v4 14/15] ui: sdn: add evpn overview panel
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (27 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 13/15] ui: sdn: add component for creatin evpn zones Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 15/15] ui: sdn: add evpn panel to main menu Stefan Hanreich
                   ` (3 subsequent siblings)
  32 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

This panel shows an overview of the state of SDN EVPN Zones across
multiple remotes. It includes two different views: a per-remote and a
per-VRF view. For details on the specific views consult the respective
commits. It handles the fetching of data and passing them to the
specific child components and it also handles the dialogues for
creating new EVPN entities (zones, vnets).

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 ui/src/sdn/evpn/evpn_panel.rs | 262 ++++++++++++++++++++++++++++++++++
 ui/src/sdn/evpn/mod.rs        |   3 +
 2 files changed, 265 insertions(+)
 create mode 100644 ui/src/sdn/evpn/evpn_panel.rs

diff --git a/ui/src/sdn/evpn/evpn_panel.rs b/ui/src/sdn/evpn/evpn_panel.rs
new file mode 100644
index 0000000..89e2e58
--- /dev/null
+++ b/ui/src/sdn/evpn/evpn_panel.rs
@@ -0,0 +1,262 @@
+use futures::try_join;
+use std::rc::Rc;
+
+use anyhow::Error;
+use yew::virtual_dom::{VComp, VNode};
+use yew::{html, Callback, Html, Properties};
+
+use pdm_client::types::{ListController, ListControllersType, ListVnet, ListZone, ListZonesType};
+use proxmox_yew_comp::{LoadableComponent, LoadableComponentContext, LoadableComponentMaster};
+
+use pwt::css::{AlignItems, FlexFit, JustifyContent};
+use pwt::props::{ContainerBuilder, EventSubscriber, StorageLocation, WidgetBuilder};
+use pwt::state::NavigationContainer;
+use pwt::tr;
+use pwt::widget::menu::{Menu, MenuButton, MenuItem};
+use pwt::widget::{Button, Column, MiniScrollMode, TabBarItem, TabPanel, Toolbar};
+
+use crate::pdm_client;
+use crate::sdn::evpn::{AddVnetWindow, AddZoneWindow, RemoteTree, VrfTree};
+
+#[derive(PartialEq, Properties)]
+pub struct EvpnPanel {}
+
+impl Default for EvpnPanel {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl EvpnPanel {
+    pub fn new() -> Self {
+        Self {}
+    }
+}
+
+impl From<EvpnPanel> for VNode {
+    fn from(value: EvpnPanel) -> Self {
+        let comp = VComp::new::<LoadableComponentMaster<EvpnPanelComponent>>(Rc::new(value), None);
+        VNode::from(comp)
+    }
+}
+
+pub enum EvpnPanelMsg {
+    Reload,
+    LoadFinished {
+        controllers: Rc<Vec<ListController>>,
+        zones: Rc<Vec<ListZone>>,
+        vnets: Rc<Vec<ListVnet>>,
+    },
+}
+
+#[derive(Debug, PartialEq)]
+pub enum EvpnPanelViewState {
+    AddZone,
+    AddVnet,
+}
+
+async fn load_zones() -> Result<Vec<ListZone>, Error> {
+    let client = pdm_client();
+    let data = client
+        .pve_sdn_list_zones(false, true, ListZonesType::Evpn)
+        .await?;
+    Ok(data)
+}
+
+async fn load_controllers() -> Result<Vec<ListController>, Error> {
+    let client = pdm_client();
+    let data = client
+        .pve_sdn_list_controllers(false, true, ListControllersType::Evpn)
+        .await?;
+    Ok(data)
+}
+
+async fn load_vnets() -> Result<Vec<ListVnet>, Error> {
+    let client = pdm_client();
+    let data = client.pve_sdn_list_vnets(false, true).await?;
+    Ok(data)
+}
+
+pub struct EvpnPanelComponent {
+    controllers: Rc<Vec<ListController>>,
+    zones: Rc<Vec<ListZone>>,
+    vnets: Rc<Vec<ListVnet>>,
+    initial_load: bool,
+}
+
+impl EvpnPanelComponent {
+    fn create_toolbar(&self, ctx: &LoadableComponentContext<Self>) -> Toolbar {
+        let on_add_zone = ctx
+            .link()
+            .change_view_callback(|_| Some(EvpnPanelViewState::AddZone));
+
+        let on_add_vnet = ctx
+            .link()
+            .change_view_callback(|_| Some(EvpnPanelViewState::AddVnet));
+
+        let on_refresh = ctx.link().callback(|_| EvpnPanelMsg::Reload);
+
+        let add_menu = Menu::new()
+            .with_item(
+                MenuItem::new(tr!("Zone"))
+                    .icon_class("fa fa-th")
+                    .on_select(on_add_zone),
+            )
+            .with_item(
+                MenuItem::new(tr!("VNet"))
+                    .icon_class("fa fa-sdn-vnet")
+                    .on_select(on_add_vnet),
+            );
+
+        Toolbar::new()
+            .class("pwt-w-100")
+            .class("pwt-overflow-hidden")
+            .class("pwt-border-bottom")
+            .with_child(MenuButton::new(tr!("Add")).show_arrow(true).menu(add_menu))
+            .with_flex_spacer()
+            .with_child(Button::refresh(ctx.loading()).onclick(on_refresh))
+    }
+}
+
+impl LoadableComponent for EvpnPanelComponent {
+    type Properties = EvpnPanel;
+    type Message = EvpnPanelMsg;
+    type ViewState = EvpnPanelViewState;
+
+    fn create(_ctx: &LoadableComponentContext<Self>) -> Self {
+        Self {
+            initial_load: true,
+            controllers: Default::default(),
+            zones: Default::default(),
+            vnets: Default::default(),
+        }
+    }
+
+    fn load(
+        &self,
+        ctx: &LoadableComponentContext<Self>,
+    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), Error>>>> {
+        let link = ctx.link().clone();
+
+        Box::pin(async move {
+            let (controllers, zones, vnets) =
+                try_join!(load_controllers(), load_zones(), load_vnets())?;
+
+            link.send_message(Self::Message::LoadFinished {
+                controllers: Rc::new(controllers),
+                zones: Rc::new(zones),
+                vnets: Rc::new(vnets),
+            });
+
+            Ok(())
+        })
+    }
+
+    fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
+        match msg {
+            Self::Message::LoadFinished {
+                controllers,
+                zones,
+                vnets,
+            } => {
+                self.controllers = controllers;
+                self.zones = zones;
+                self.vnets = vnets;
+                self.initial_load = false;
+
+                return true;
+            }
+            Self::Message::Reload => {
+                ctx.link().send_reload();
+            }
+        }
+
+        false
+    }
+
+    fn main_view(&self, ctx: &LoadableComponentContext<Self>) -> Html {
+        let panel = TabPanel::new()
+            .state_id(StorageLocation::session("EvpnPanelState"))
+            .class(pwt::css::FlexFit)
+            .router(true)
+            .scroll_mode(MiniScrollMode::Arrow)
+            .with_item(
+                TabBarItem::new()
+                    .key("remotes")
+                    .label(tr!("Remotes"))
+                    .icon_class("fa fa-server"),
+                Column::new()
+                    .class(pwt::css::FlexFit)
+                    .with_child(self.create_toolbar(ctx))
+                    .with_child(if self.initial_load {
+                        VNode::from(
+                            Column::new()
+                                .class(FlexFit)
+                                .class(AlignItems::Center)
+                                .class(JustifyContent::Center)
+                                .with_child(html! {<i class={"pwt-loading-icon"} />}),
+                        )
+                    } else {
+                        VNode::from(RemoteTree::new(
+                            self.zones.clone(),
+                            self.vnets.clone(),
+                            self.controllers.clone(),
+                        ))
+                    }),
+            )
+            .with_item(
+                TabBarItem::new()
+                    .key("vrfs")
+                    .label(tr!("IP-VRFs"))
+                    .icon_class("fa fa-th"),
+                Column::new()
+                    .class(pwt::css::FlexFit)
+                    .with_child(self.create_toolbar(ctx))
+                    .with_child(if self.initial_load {
+                        VNode::from(
+                            Column::new()
+                                .class(FlexFit)
+                                .class(AlignItems::Center)
+                                .class(JustifyContent::Center)
+                                .with_child(html! {<i class={"pwt-loading-icon"} />}),
+                        )
+                    } else {
+                        VNode::from(VrfTree::new(
+                            self.zones.clone(),
+                            self.vnets.clone(),
+                            self.controllers.clone(),
+                        ))
+                    }),
+            );
+
+        let navigation_container = NavigationContainer::new().with_child(panel);
+
+        Column::new()
+            .class(pwt::css::FlexFit)
+            .with_child(navigation_container)
+            .into()
+    }
+
+    fn dialog_view(
+        &self,
+        ctx: &LoadableComponentContext<Self>,
+        view_state: &Self::ViewState,
+    ) -> Option<Html> {
+        let scope = ctx.link().clone();
+
+        let on_success = Callback::from(move |upid: String| {
+            scope.show_task_log(upid, None);
+        });
+
+        let on_close = ctx.link().clone().change_view_callback(|_| None);
+
+        Some(match view_state {
+            EvpnPanelViewState::AddZone => {
+                AddZoneWindow::new(self.controllers.clone(), on_success, on_close).into()
+            }
+            EvpnPanelViewState::AddVnet => {
+                AddVnetWindow::new(self.zones.clone(), on_success, on_close).into()
+            }
+        })
+    }
+}
diff --git a/ui/src/sdn/evpn/mod.rs b/ui/src/sdn/evpn/mod.rs
index adcf272..1948ecf 100644
--- a/ui/src/sdn/evpn/mod.rs
+++ b/ui/src/sdn/evpn/mod.rs
@@ -1,3 +1,6 @@
+mod evpn_panel;
+pub use evpn_panel::EvpnPanel;
+
 mod remote_tree;
 pub use remote_tree::RemoteTree;
 
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v4 15/15] ui: sdn: add evpn panel to main menu
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (28 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 14/15] ui: sdn: add evpn overview panel Stefan Hanreich
@ 2025-09-04  8:18 ` Stefan Hanreich
  2025-09-04  9:10 ` [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Dominik Csapak
                   ` (2 subsequent siblings)
  32 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-04  8:18 UTC (permalink / raw)
  To: pdm-devel

Expose the new EVPN overview in the main menu. In the future this
might move below a dedicated SDN top-level entry, but since we have
only one view for now, display it as a top-level entry.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 ui/src/main_menu.rs | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/ui/src/main_menu.rs b/ui/src/main_menu.rs
index 4dcf881..7eac775 100644
--- a/ui/src/main_menu.rs
+++ b/ui/src/main_menu.rs
@@ -17,6 +17,7 @@ use proxmox_yew_comp::{NotesView, XTermJs};
 use pdm_api_types::remotes::RemoteType;
 
 use crate::remotes::RemotesPanel;
+use crate::sdn::evpn::EvpnPanel;
 use crate::{
     AccessControl, CertificatesPanel, Dashboard, RemoteList, ServerAdministration,
     SystemConfiguration,
@@ -247,6 +248,15 @@ impl Component for PdmMainMenu {
             admin_submenu,
         );
 
+        register_view(
+            &mut menu,
+            &mut content,
+            tr!("EVPN"),
+            "evpn",
+            Some("fa fa-sitemap"),
+            |_| EvpnPanel::new().into(),
+        );
+
         let mut remote_submenu = Menu::new();
 
         for remote in self.remote_list_cache.iter() {
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* Re: [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (29 preceding siblings ...)
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 15/15] ui: sdn: add evpn panel to main menu Stefan Hanreich
@ 2025-09-04  9:10 ` Dominik Csapak
  2025-09-04 13:27 ` [pdm-devel] applied-series: " Wolfgang Bumiller
  2025-09-05 12:37 ` [pdm-devel] " Hannes Duerr
  32 siblings, 0 replies; 40+ messages in thread
From: Dominik Csapak @ 2025-09-04  9:10 UTC (permalink / raw)
  To: Proxmox Datacenter Manager development discussion, Stefan Hanreich

Changes look good to me (i diffed the older version to this one instead
of looking at every patch again)

Tested again, and everything went as expected.

Consider this version:

Reviewed-by: Dominik Csapak <d.csapak@proxmox.com>
Tested-by: Dominik Csapak <d.csapak@proxmox.com>

On 9/4/25 10:19 AM, Stefan Hanreich wrote:
> ## Introduction
> 
> This patch series adds a new panel to the PDM that shows an overview of the
> current state of all EVPN zones across all remotes. It includes two different
> tree views:
> 
> * IP-VRFs: that shows the contents of all IP-VRFs (identified by their Route
>    Target = ASN:VNI) across all remotes.
> * Zones: that shows the contents of a specific zone on a specific remote.
> 
> For more information on the two tree views, consult the respective commits that
> introduce the components.
> 
> The panel also allows users to create new Zones / VNets on multiple remotes
> simultaneously by utilizing the new SDN locking functionality.
> 
> I have provided prebuilt packages on the share in the folder pdm-evpn.
> 
> This patch series requires the ParallelFetcher patch series from Lukas in order
> to work.
> 
> ## API
> 
> It introduces the following API endpoints on PDM:
> 
> /sdn
>      GET /controllers - list the controllers of all remotes
>      GET /zones - list the zones of all remotes
>      POST /zones - create a zone on multiple remotes
>      GET /vnets - list the vnets of all remotes
>      POST /vnets - create a vnet on multiple remotes
> 
> 
> ## Additional remarks
> 
> This patch series contains some preparatory patches that are not directly
> related to the implemented functionality:
> 
> * One fix for proxmox-schema so values that are larger than i32 can be used in
>    the integer schema definition (required for e.g. 64-bit ASNs)
> * Add JSONSchema to a lot of SDN API endpoints that were previously undocumented
> 
> I have sent them initially as separate patch series, but since they are a hard
> requirement for this patch series I have merged all of them into one patch
> series now. This way it is easier to keep track of the requirements.
> 
> 
> ## Open questions for reviewers
> 
> * The LockedSdnClient(s) are abstractions for locked SDN remotes. I'm still a
> bit unsure about the design / implementation but for future features I will be
> making more complex changes across multiple remotes so I figured an abstraction
> for this will come in handy in the future.
> 
> I'd love some inputs / opinions on the API design as well as the general concept
> of locking config -> making changes -> rolling back / applying.
> 
> I will work on a more sophisticated implementation utilizing tokio-specific
> functions in the following days, but I wanted to get the patch series out now
> and validate the API / general idea.
> 
> * We might wanna move the EvpnRouteTarget type out of the UI, even though it is
> currently only used there.
> 
> * Should we introduce a caching mechanism for the SDN API calls?
> 
> I have shortly talked about this with @Lukas, but we decided against
> implementing such a mechanism for now after some deliberation.
> 
> Showing outdated information is particularly problematic with configuration,
> especially because the create dialogues rely on that information.
> 
> After creating a new zone / vnet we would have to hit the remotes anyway, in
> order to be able to show the updated data immediately.
> 
> The downside is of course a long load time for the EVPN panel, as well as a long
> load if even one of the remotes is not available.
> 
> For an initial release I think it is fine to go forward without caching and see
> how it works out in practice based on reports from our users. Any input on this
> matter would be greatly appreciated!
> 
> 
> ## Future Work
> * show the output of the new status API calls created by Gabriel in the views.
> * add a functionality for grouping remotes together, instead of implicitly
>    grouping them based on ASN:VNI
> * introduce a caching mechanism for the SDN API calls (?)
> * integration tests with mocked SDN clients
> * add some QoL to the UI (e.g expand/collapse all)
> 
> 
> Huge thanks to @Lukas and @Dominik for helping me greatly on moving this patch
> series forward the last few days!
> 
> ## Changelog
> 
> Changes from v3 (Thanks @Shannon, @Wolfang):
> * created dedicated verification functions for SDN IDs
> * improved / fixed regex for SDN ids in the process
> * improved API documentation for SDN zones in PVE
> * use new verification functions in PDM types as well
> * SDN client prints the correct remote when releasing the lock
> 
> Changes from v2:
> * detect invalid response from rollback endpoint to gracefully handle unpatched
>    libpve-network-api-perl
> * use create_toolbar instead of implementing a whole component
> * pass is_loading to Refresh button
> * show spinner on initial load instead of empty trees
> * improved default sorting order for remotes tree
> * sort PveClients in LockedSdnClients to provide ordered output
> * use HashSet for all list endpoints for deduplication and efficient filtering
> 
> Changes from v1:
> * detect legacy PVE remotes without SDN locking API capability
> * remove already applied patch
> * parallelize list endpoints via Lukas' ParallelFetcher
> * reversed toolbar / grid order in EVPN panel
> * updated and improved commit messages
> * added missing translation macro invocations
> * replaced thread_local in components
> * store columns in component to avoid re-creating them on update
> * add better error message in add_zone/vnet dialogues if there is no
>    controller / zone
> * remove unused message from vrf/remote tree components
> * use update_root_tree for restoring tree state
> * moved EVPN above remotes in the main menu
> * added instructions on how to unlock SDN configuration in cases of errors
> 
> Changes from RFC v2:
> * rebased on top of current master
> * improved error handling for the yew components considerably
> * tinkered with column sizes in the remote view
> * preserve collapsed state on refresh
> * fix SDN ID schema definition
> * improved EVPN icon
> * moved task descriptions from yew-comp to pdm
> * improved default sorting order for the remote view
> 
> Changes from RFC v1:
> * overhauled the structure of the trees completely
>    * split the initial tree view into two distinct tree views
>    * changed the grouping of elements
>    * improved and unified the terms used across all UI elements
> * improved toolbar design
> * removed the controller data table, since the tree views should now include
>    that information
> * improved locked SDN client and added a collection type for locked SDN clients
> * improved error handling and logging considerably for the worker tasks
> 
> 
> ## Dependencies:
> pbs-api-types depends on proxmox-schema
> proxmox-api-types depends on proxmox-schema
> proxmox-backup depends on proxmox-schema
> proxmox-datacenter-manager depends on proxmox-schema
> 
> proxmox-api-types depends on pve-network
> proxmox-datacenter-manager depends on proxmox-api-types
> proxmox-datacenter-manager depends on pve-network
> 
> proxmox:
> 
> Stefan Hanreich (2):
>    schema: use i64 for minimum / maximum / default integer values
>    pbs-api-types: fix values for integer schemas
> 
>   pbs-api-types/src/datastore.rs  |  6 +++---
>   proxmox-schema/src/de/mod.rs    |  3 +--
>   proxmox-schema/src/de/verify.rs | 13 ++++++++-----
>   proxmox-schema/src/schema.rs    | 18 +++++++++---------
>   4 files changed, 21 insertions(+), 19 deletions(-)
> 
> 
> proxmox-backup:
> 
> Stefan Hanreich (1):
>    api: change integer schema parameters to i64
> 
>   pbs-tape/src/bin/pmt.rs           |  6 +++---
>   proxmox-backup-client/src/main.rs |  2 +-
>   pxar-bin/src/main.rs              |  6 +++---
>   src/api2/backup/upload_chunk.rs   | 15 ++++++---------
>   4 files changed, 13 insertions(+), 16 deletions(-)
> 
> 
> pve-network:
> 
> Stefan Hanreich (6):
>    sdn: api: return null for rollback / lock endpoints
>    controllers: fix maximum value for ASN
>    api: add state standard option
>    api: controllers: update schema of endpoints
>    api: vnets: update schema of endpoints
>    api: zones: update schema of endpoints
> 
>   src/PVE/API2/Network/SDN.pm                   |   4 +
>   src/PVE/API2/Network/SDN/Controllers.pm       | 116 +++++++++-
>   src/PVE/API2/Network/SDN/Vnets.pm             |  92 +++++++-
>   src/PVE/API2/Network/SDN/Zones.pm             | 204 ++++++++++++++++--
>   src/PVE/Network/SDN.pm                        |  10 +
>   src/PVE/Network/SDN/Controllers/BgpPlugin.pm  |   7 +-
>   src/PVE/Network/SDN/Controllers/EvpnPlugin.pm |   2 +-
>   src/PVE/Network/SDN/Controllers/IsisPlugin.pm |   6 +-
>   src/PVE/Network/SDN/VnetPlugin.pm             |  21 +-
>   src/PVE/Network/SDN/Zones/EvpnPlugin.pm       |  22 +-
>   src/PVE/Network/SDN/Zones/QinQPlugin.pm       |   6 +-
>   src/PVE/Network/SDN/Zones/VlanPlugin.pm       |   1 +
>   src/PVE/Network/SDN/Zones/VxlanPlugin.pm      |  16 +-
>   13 files changed, 459 insertions(+), 48 deletions(-)
> 
> 
> proxmox-api-types:
> 
> Stefan Hanreich (6):
>    sdn: add list/create zone endpoints
>    sdn: add list/create vnet endpoints
>    sdn: add list/create controller endpoints
>    sdn: add sdn configuration locking endpoints
>    tasks: add helper for querying successfully finished tasks
>    sdn: add helpers for pending values
> 
>   pve-api-types/generate.pl            | 38 +++++++++++++++++++
>   pve-api-types/src/lib.rs             |  1 +
>   pve-api-types/src/sdn.rs             | 33 ++++++++++++++++
>   pve-api-types/src/types/mod.rs       |  4 ++
>   pve-api-types/src/types/verifiers.rs | 56 ++++++++++++++++++++++++++++
>   5 files changed, 132 insertions(+)
>   create mode 100644 pve-api-types/src/sdn.rs
> 
> 
> proxmox-datacenter-manager:
> 
> Stefan Hanreich (15):
>    server: add locked sdn client helpers
>    ui: pve: sdn: add descriptions for sdn tasks
>    api: sdn: add list_zones endpoint
>    api: sdn: add create_zone endpoint
>    api: sdn: add list_vnets endpoint
>    api: sdn: add create_vnet endpoint
>    api: sdn: add list_controllers endpoint
>    ui: sdn: add EvpnRouteTarget type
>    ui: sdn: add vnet icon
>    ui: sdn: add view for showing evpn zones
>    ui: sdn: add view for showing ip vrfs
>    ui: sdn: add component for creating evpn vnets
>    ui: sdn: add component for creatin evpn zones
>    ui: sdn: add evpn overview panel
>    ui: sdn: add evpn panel to main menu
> 
>   lib/pdm-api-types/Cargo.toml      |   2 +
>   lib/pdm-api-types/src/lib.rs      |   2 +
>   lib/pdm-api-types/src/sdn.rs      | 171 ++++++++++
>   lib/pdm-client/src/lib.rs         |  61 ++++
>   server/src/api/mod.rs             |   2 +
>   server/src/api/sdn/controllers.rs | 114 +++++++
>   server/src/api/sdn/mod.rs         |  17 +
>   server/src/api/sdn/vnets.rs       | 180 +++++++++++
>   server/src/api/sdn/zones.rs       | 206 +++++++++++++
>   server/src/lib.rs                 |   1 +
>   server/src/sdn_client.rs          | 432 ++++++++++++++++++++++++++
>   ui/css/pdm.scss                   |  14 +-
>   ui/images/icon-sdn-vnet.svg       |   6 +
>   ui/src/lib.rs                     |   2 +
>   ui/src/main_menu.rs               |  10 +
>   ui/src/sdn/evpn/add_vnet.rs       | 313 +++++++++++++++++++
>   ui/src/sdn/evpn/add_zone.rs       | 328 ++++++++++++++++++++
>   ui/src/sdn/evpn/evpn_panel.rs     | 262 ++++++++++++++++
>   ui/src/sdn/evpn/mod.rs            |  41 +++
>   ui/src/sdn/evpn/remote_tree.rs    | 496 ++++++++++++++++++++++++++++++
>   ui/src/sdn/evpn/vrf_tree.rs       | 409 ++++++++++++++++++++++++
>   ui/src/sdn/mod.rs                 |   1 +
>   ui/src/tasks.rs                   |   4 +
>   23 files changed, 3073 insertions(+), 1 deletion(-)
>   create mode 100644 lib/pdm-api-types/src/sdn.rs
>   create mode 100644 server/src/api/sdn/controllers.rs
>   create mode 100644 server/src/api/sdn/mod.rs
>   create mode 100644 server/src/api/sdn/vnets.rs
>   create mode 100644 server/src/api/sdn/zones.rs
>   create mode 100644 server/src/sdn_client.rs
>   create mode 100644 ui/images/icon-sdn-vnet.svg
>   create mode 100644 ui/src/sdn/evpn/add_vnet.rs
>   create mode 100644 ui/src/sdn/evpn/add_zone.rs
>   create mode 100644 ui/src/sdn/evpn/evpn_panel.rs
>   create mode 100644 ui/src/sdn/evpn/mod.rs
>   create mode 100644 ui/src/sdn/evpn/remote_tree.rs
>   create mode 100644 ui/src/sdn/evpn/vrf_tree.rs
>   create mode 100644 ui/src/sdn/mod.rs
> 
> 
> Summary over all repositories:
>    49 files changed, 3698 insertions(+), 84 deletions(-)
> 



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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] applied: [PATCH proxmox v4 1/2] schema: use i64 for minimum / maximum / default integer values
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox v4 1/2] schema: use i64 for minimum / maximum / default integer values Stefan Hanreich
@ 2025-09-04 10:03   ` Wolfgang Bumiller
  0 siblings, 0 replies; 40+ messages in thread
From: Wolfgang Bumiller @ 2025-09-04 10:03 UTC (permalink / raw)
  To: Stefan Hanreich; +Cc: pdm-devel

applied, thanks

On Thu, Sep 04, 2025 at 10:18:26AM +0200, Stefan Hanreich wrote:
> IntegerSchema used isize for the minimum / maximum parameters. On 32
> bit targets (wasm for instance) API defintions with minimum / maximum
> sizes outside the i32 range would fail.
> 
> This also changes the u64 deserialize to fail if it encounters a value
> that cannot be represented as i64 instead of simply casting it to i64
> and moving on.
> 
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
>  proxmox-schema/src/de/mod.rs    |  3 +--
>  proxmox-schema/src/de/verify.rs | 13 ++++++++-----
>  proxmox-schema/src/schema.rs    | 18 +++++++++---------
>  3 files changed, 18 insertions(+), 16 deletions(-)
> 
> diff --git a/proxmox-schema/src/de/mod.rs b/proxmox-schema/src/de/mod.rs
> index eca835e3..1f14503e 100644
> --- a/proxmox-schema/src/de/mod.rs
> +++ b/proxmox-schema/src/de/mod.rs
> @@ -173,8 +173,7 @@ impl<'de> de::Deserializer<'de> for SchemaDeserializer<'de, '_> {
>                      .map_err(|_| Error::msg(format!("not a boolean: {:?}", self.input)))?,
>              ),
>              Schema::Integer(schema) => {
> -                // FIXME: isize vs explicit i64, needs fixing in schema check_constraints api
> -                let value: isize = self
> +                let value: i64 = self
>                      .input
>                      .parse()
>                      .map_err(|_| Error::msg(format!("not an integer: {:?}", self.input)))?;
> diff --git a/proxmox-schema/src/de/verify.rs b/proxmox-schema/src/de/verify.rs
> index 67a8c9e8..917f956b 100644
> --- a/proxmox-schema/src/de/verify.rs
> +++ b/proxmox-schema/src/de/verify.rs
> @@ -170,7 +170,7 @@ impl<'de> de::Visitor<'de> for Visitor {
>  
>      fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
>          match self.0 {
> -            Schema::Integer(schema) => match schema.check_constraints(v as isize) {
> +            Schema::Integer(schema) => match schema.check_constraints(v) {
>                  Ok(()) => Ok(Verifier),
>                  Err(err) => Err(E::custom(err)),
>              },
> @@ -180,10 +180,13 @@ impl<'de> de::Visitor<'de> for Visitor {
>  
>      fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
>          match self.0 {
> -            Schema::Integer(schema) => match schema.check_constraints(v as isize) {
> -                Ok(()) => Ok(Verifier),
> -                Err(err) => Err(E::custom(err)),
> -            },
> +            Schema::Integer(schema) => {
> +                let val = v.try_into().or_else(|err| Err(E::custom(err)))?;
> +                match schema.check_constraints(val) {
> +                    Ok(()) => Ok(Verifier),
> +                    Err(err) => Err(E::custom(err)),
> +                }
> +            }
>              _ => Err(E::invalid_type(Unexpected::Unsigned(v), &self)),
>          }
>      }
> diff --git a/proxmox-schema/src/schema.rs b/proxmox-schema/src/schema.rs
> index 1b26c45b..e4ad971b 100644
> --- a/proxmox-schema/src/schema.rs
> +++ b/proxmox-schema/src/schema.rs
> @@ -228,11 +228,11 @@ impl BooleanSchema {
>  pub struct IntegerSchema {
>      pub description: &'static str,
>      /// Optional minimum.
> -    pub minimum: Option<isize>,
> +    pub minimum: Option<i64>,
>      /// Optional maximum.
> -    pub maximum: Option<isize>,
> +    pub maximum: Option<i64>,
>      /// Optional default.
> -    pub default: Option<isize>,
> +    pub default: Option<i64>,
>  }
>  
>  impl IntegerSchema {
> @@ -250,17 +250,17 @@ impl IntegerSchema {
>          self
>      }
>  
> -    pub const fn default(mut self, default: isize) -> Self {
> +    pub const fn default(mut self, default: i64) -> Self {
>          self.default = Some(default);
>          self
>      }
>  
> -    pub const fn minimum(mut self, minimum: isize) -> Self {
> +    pub const fn minimum(mut self, minimum: i64) -> Self {
>          self.minimum = Some(minimum);
>          self
>      }
>  
> -    pub const fn maximum(mut self, maximum: isize) -> Self {
> +    pub const fn maximum(mut self, maximum: i64) -> Self {
>          self.maximum = Some(maximum);
>          self
>      }
> @@ -269,7 +269,7 @@ impl IntegerSchema {
>          Schema::Integer(self)
>      }
>  
> -    pub fn check_constraints(&self, value: isize) -> Result<(), Error> {
> +    pub fn check_constraints(&self, value: i64) -> Result<(), Error> {
>          if let Some(minimum) = self.minimum {
>              if value < minimum {
>                  bail!(
> @@ -296,7 +296,7 @@ impl IntegerSchema {
>      /// Verify JSON value using an `IntegerSchema`.
>      pub fn verify_json(&self, data: &Value) -> Result<(), Error> {
>          if let Some(value) = data.as_i64() {
> -            self.check_constraints(value as isize)
> +            self.check_constraints(value)
>          } else {
>              bail!("Expected integer value.");
>          }
> @@ -1406,7 +1406,7 @@ impl Schema {
>                  Value::Bool(res)
>              }
>              Schema::Integer(integer_schema) => {
> -                let res: isize = value_str.parse()?;
> +                let res: i64 = value_str.parse()?;
>                  integer_schema.check_constraints(res)?;
>                  Value::Number(res.into())
>              }
> -- 
> 2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] applied: [PATCH proxmox v4 2/2] pbs-api-types: fix values for integer schemas
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox v4 2/2] pbs-api-types: fix values for integer schemas Stefan Hanreich
@ 2025-09-04 10:03   ` Wolfgang Bumiller
  0 siblings, 0 replies; 40+ messages in thread
From: Wolfgang Bumiller @ 2025-09-04 10:03 UTC (permalink / raw)
  To: Stefan Hanreich; +Cc: pdm-devel

applied and bumped schema to 5

On Thu, Sep 04, 2025 at 10:18:27AM +0200, Stefan Hanreich wrote:
> Because of the change from isize to i64 some casts have to be adjusted
> so the types for the maximum / default values are correct again.
> 
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
>  pbs-api-types/src/datastore.rs | 6 +++---
>  1 file changed, 3 insertions(+), 3 deletions(-)
> 
> diff --git a/pbs-api-types/src/datastore.rs b/pbs-api-types/src/datastore.rs
> index ee94ccad..fe73cbc4 100644
> --- a/pbs-api-types/src/datastore.rs
> +++ b/pbs-api-types/src/datastore.rs
> @@ -93,14 +93,14 @@ pub const BACKUP_NAMESPACE_SCHEMA: Schema = StringSchema::new("Namespace.")
>  pub const NS_MAX_DEPTH_SCHEMA: Schema =
>      IntegerSchema::new("How many levels of namespaces should be operated on (0 == no recursion)")
>          .minimum(0)
> -        .maximum(MAX_NAMESPACE_DEPTH as isize)
> -        .default(MAX_NAMESPACE_DEPTH as isize)
> +        .maximum(MAX_NAMESPACE_DEPTH as i64)
> +        .default(MAX_NAMESPACE_DEPTH as i64)
>          .schema();
>  
>  pub const NS_MAX_DEPTH_REDUCED_SCHEMA: Schema =
>  IntegerSchema::new("How many levels of namespaces should be operated on (0 == no recursion, empty == automatic full recursion, namespace depths reduce maximum allowed value)")
>      .minimum(0)
> -    .maximum(MAX_NAMESPACE_DEPTH as isize)
> +    .maximum(MAX_NAMESPACE_DEPTH as i64)
>      .schema();
>  
>  pub const DATASTORE_SCHEMA: Schema = StringSchema::new("Datastore name.")
> -- 
> 2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] appled: [PATCH pve-network v4 1/6] sdn: api: return null for rollback / lock endpoints
  2025-09-04  8:18 ` [pdm-devel] [PATCH pve-network v4 1/6] sdn: api: return null for rollback / lock endpoints Stefan Hanreich
@ 2025-09-04 12:31   ` Wolfgang Bumiller
  0 siblings, 0 replies; 40+ messages in thread
From: Wolfgang Bumiller @ 2025-09-04 12:31 UTC (permalink / raw)
  To: Stefan Hanreich; +Cc: pdm-devel

applied all pve-network patches, thanks

On Thu, Sep 04, 2025 at 10:18:29AM +0200, Stefan Hanreich wrote:
> lock_sdn_config can return a boolean value, which will then in turn
> get returned as data from the API calls. Since we hint type null here,
> this leads to problems with the pve-api-client in rust. Fix the return
> value for this API call by adding an explicit return statement.
> 
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
>  src/PVE/API2/Network/SDN.pm | 4 ++++
>  1 file changed, 4 insertions(+)
> 
> diff --git a/src/PVE/API2/Network/SDN.pm b/src/PVE/API2/Network/SDN.pm
> index af00b1a..88b229c 100644
> --- a/src/PVE/API2/Network/SDN.pm
> +++ b/src/PVE/API2/Network/SDN.pm
> @@ -192,6 +192,8 @@ __PACKAGE__->register_method({
>                  $param->{'lock-token'},
>              );
>          }
> +
> +        return;
>      },
>  });
>  
> @@ -247,6 +249,8 @@ __PACKAGE__->register_method({
>          PVE::Network::SDN::lock_sdn_config(
>              $rollback, "could not rollback SDN configuration", $lock_token,
>          );
> +
> +        return;
>      },
>  });
>  
> -- 
> 2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] applied: [PATCH proxmox-api-types v4 1/6] sdn: add list/create zone endpoints
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-api-types v4 1/6] sdn: add list/create zone endpoints Stefan Hanreich
@ 2025-09-04 12:42   ` Wolfgang Bumiller
  0 siblings, 0 replies; 40+ messages in thread
From: Wolfgang Bumiller @ 2025-09-04 12:42 UTC (permalink / raw)
  To: Stefan Hanreich; +Cc: pdm-devel

applied all api-types patches, thanks

On Thu, Sep 04, 2025 at 10:18:35AM +0200, Stefan Hanreich wrote:
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
>  pve-api-types/generate.pl            | 18 +++++++++
>  pve-api-types/src/types/verifiers.rs | 56 ++++++++++++++++++++++++++++
>  2 files changed, 74 insertions(+)
> 
> diff --git a/pve-api-types/generate.pl b/pve-api-types/generate.pl
> index cd08f3d..dc45d98 100644
> --- a/pve-api-types/generate.pl
> +++ b/pve-api-types/generate.pl
> @@ -79,6 +79,8 @@ Schema2Rust::register_format('bridge-pair' => { code => 'verifiers::verify_bridg
>  
>  Schema2Rust::register_format('pve-task-status-type' => { regex => '^(?i:ok|error|warning|unknown)$' });
>  
> +Schema2Rust::register_format('pve-sdn-zone-id' => { code => 'verifiers::verify_sdn_id' });
> +
>  Schema2Rust::register_enum_variant('PveVmCpuConfReportedModel::486' => 'I486');
>  Schema2Rust::register_enum_variant('QemuConfigEfidisk0Efitype::2m' => 'Mb2');
>  Schema2Rust::register_enum_variant('QemuConfigEfidisk0Efitype::4m' => 'Mb4');
> @@ -104,6 +106,10 @@ Schema2Rust::register_format('pve-iface' => { regex => '^[a-zA-Z][a-zA-Z0-9_]{1,
>  
>  Schema2Rust::register_format('pve-vlan-id-or-range' => { code => 'verifiers::verify_vlan_id_or_range' });
>  
> +Schema2Rust::register_format('pve-sdn-bgp-rt' => { code => 'verifiers::verify_sdn_bgp_rt' });
> +Schema2Rust::register_format('pve-sdn-controller-id' => { code => 'verifiers::verify_sdn_controller_id' });
> +Schema2Rust::register_format('pve-sdn-isis-net' => { regex => '^[a-fA-F0-9]{2}(\.[a-fA-F0-9]{4}){3,9}\.[a-fA-F0-9]{2}$' });
> +
>  # This is used as both a task status and guest status.
>  Schema2Rust::generate_enum('IsRunning', {
>      type => 'string',
> @@ -328,6 +334,18 @@ api(GET => '/nodes/{node}/apt/update', 'list_available_updates', 'return-name' =
>  api(POST => '/nodes/{node}/apt/update', 'update_apt_database', 'output-type' => 'PveUpid', 'param-name' => 'AptUpdateParams');
>  api(GET => '/nodes/{node}/apt/changelog', 'get_package_changelog', 'output-type' => 'String');
>  
> +Schema2Rust::generate_enum('SdnObjectState', {
> +    type => 'string',
> +    description => "The state of an SDN object.",
> +    enum => ['new', 'deleted', 'changed'],
> +});
> +
> +api(GET => '/cluster/sdn/zones', 'list_zones', 'return-name' => 'SdnZone');
> +Schema2Rust::derive('SdnZone' => 'Clone', 'PartialEq');
> +Schema2Rust::derive('SdnZonePending' => 'Clone', 'PartialEq');
> +api(POST => '/cluster/sdn/zones', 'create_zone', 'param-name' => 'CreateZone');
> +Schema2Rust::derive('CreateZone' => 'Clone', 'PartialEq');
> +
>  # NOW DUMP THE CODE:
>  #
>  # We generate one file for API types, and one for API method calls.
> diff --git a/pve-api-types/src/types/verifiers.rs b/pve-api-types/src/types/verifiers.rs
> index e0ad4ca..9a88118 100644
> --- a/pve-api-types/src/types/verifiers.rs
> +++ b/pve-api-types/src/types/verifiers.rs
> @@ -29,6 +29,12 @@ pub IFACE_RE = r##"^(?i)[a-z][a-z0-9_]{1,20}([:\.]\d+)?$"##;
>  
>  pub VLAN_ID_OR_RANGE = r##"^(\d+)(?:-(\d+))?$"##;
>  
> +pub SDN_ID = r##"^(?i)[a-z][a-z0-9]+$"##;
> +
> +pub SDN_CONTROLLER_ID = r##"^(?i)[a-z][a-z0-9_-]*[a-z0-9]$"##;
> +
> +pub SDN_BGP_RT = r##"^(\d+):(\d+)$"##;
> +
>  }
>  
>  pub fn verify_volume_id(s: &str) -> Result<(), Error> {
> @@ -228,3 +234,53 @@ pub fn verify_vlan_id_or_range(s: &str) -> Result<(), Error> {
>  
>      Ok(())
>  }
> +
> +pub fn verify_sdn_id(s: &str) -> Result<(), Error> {
> +    if s.len() > 8 {
> +        bail!("SDN ID cannot be longer than 8 characters")
> +    }
> +
> +    if !SDN_ID.is_match(s) {
> +        bail!("SDN ID contains illegal characters");
> +    }
> +
> +    Ok(())
> +}
> +
> +pub fn verify_sdn_controller_id(s: &str) -> Result<(), Error> {
> +    if s.len() > 64 {
> +        bail!("SDN controller ID cannot be longer than 64 characters")
> +    }
> +
> +    if !SDN_CONTROLLER_ID.is_match(s) {
> +        bail!("SDN controller ID contains illegal characters");
> +    }
> +
> +    Ok(())
> +}
> +
> +pub fn verify_sdn_bgp_rt(s: &str) -> Result<(), Error> {
> +    let captures = SDN_BGP_RT
> +        .captures(s)
> +        .ok_or_else(|| format_err!("invalid BGP RT: '{s}"))?;
> +
> +    match (captures.get(1), captures.get(2)) {
> +        (Some(asn), Some(vni)) => {
> +            asn.as_str()
> +                .parse::<u32>()
> +                .map_err(|_| format_err!("Invalid ASN in BGP RT: {s}"))?;
> +
> +            let vni: u32 = vni
> +                .as_str()
> +                .parse()
> +                .map_err(|_| format_err!("Invalid VNI in BGP RT: {s}"))?;
> +
> +            if vni > 16_777_215 {
> +                bail!("invalid VNI in BGP RT: '{s}'")
> +            }
> +        }
> +        _ => bail!("invalid BGP RT: '{s}"),
> +    }
> +
> +    Ok(())
> +}
> -- 
> 2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] applied: [PATCH proxmox-backup v4 1/1] api: change integer schema parameters to i64
  2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-backup v4 1/1] api: change integer schema parameters to i64 Stefan Hanreich
@ 2025-09-04 12:46   ` Wolfgang Bumiller
  0 siblings, 0 replies; 40+ messages in thread
From: Wolfgang Bumiller @ 2025-09-04 12:46 UTC (permalink / raw)
  To: Stefan Hanreich; +Cc: pdm-devel

applied, thanks

On Thu, Sep 04, 2025 at 10:18:28AM +0200, Stefan Hanreich wrote:
> The type for the minimum / maximum / default values for integer
> schemas has changed to i64 (from isize). Fix all call sites that are
> converting to isize to convert to i64 instead.
> 
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
>  pbs-tape/src/bin/pmt.rs           |  6 +++---
>  proxmox-backup-client/src/main.rs |  2 +-
>  pxar-bin/src/main.rs              |  6 +++---
>  src/api2/backup/upload_chunk.rs   | 15 ++++++---------
>  4 files changed, 13 insertions(+), 16 deletions(-)
> 
> diff --git a/pbs-tape/src/bin/pmt.rs b/pbs-tape/src/bin/pmt.rs
> index d70c61e9..77b61314 100644
> --- a/pbs-tape/src/bin/pmt.rs
> +++ b/pbs-tape/src/bin/pmt.rs
> @@ -28,17 +28,17 @@ use pbs_tape::{
>  
>  pub const FILE_MARK_COUNT_SCHEMA: Schema = IntegerSchema::new("File mark count.")
>      .minimum(1)
> -    .maximum(i32::MAX as isize)
> +    .maximum(i32::MAX as i64)
>      .schema();
>  
>  pub const FILE_MARK_POSITION_SCHEMA: Schema = IntegerSchema::new("File mark position (0 is BOT).")
>      .minimum(0)
> -    .maximum(i32::MAX as isize)
> +    .maximum(i32::MAX as i64)
>      .schema();
>  
>  pub const RECORD_COUNT_SCHEMA: Schema = IntegerSchema::new("Record count.")
>      .minimum(1)
> -    .maximum(i32::MAX as isize)
> +    .maximum(i32::MAX as i64)
>      .schema();
>  
>  pub const DRIVE_OPTION_SCHEMA: Schema =
> diff --git a/proxmox-backup-client/src/main.rs b/proxmox-backup-client/src/main.rs
> index 3f6c5adb..7dc2c486 100644
> --- a/proxmox-backup-client/src/main.rs
> +++ b/proxmox-backup-client/src/main.rs
> @@ -726,7 +726,7 @@ fn spawn_catalog_upload(
>                  type: Integer,
>                  description: "Max number of entries to hold in memory.",
>                  optional: true,
> -                default: pbs_client::pxar::ENCODER_MAX_ENTRIES as isize,
> +                default: pbs_client::pxar::ENCODER_MAX_ENTRIES as i64,
>              },
>              "dry-run": {
>                  type: Boolean,
> diff --git a/pxar-bin/src/main.rs b/pxar-bin/src/main.rs
> index cccbb331..cba5646b 100644
> --- a/pxar-bin/src/main.rs
> +++ b/pxar-bin/src/main.rs
> @@ -330,9 +330,9 @@ fn extract_archive(
>              "entries-max": {
>                  description: "Max number of entries loaded at once into memory",
>                  optional: true,
> -                default: ENCODER_MAX_ENTRIES as isize,
> +                default: ENCODER_MAX_ENTRIES as i64,
>                  minimum: 0,
> -                maximum: isize::MAX,
> +                maximum: i64::MAX,
>              },
>              "payload-output": {
>                  description: "'ppxar' payload output data file to create split archive.",
> @@ -354,7 +354,7 @@ async fn create_archive(
>      no_fifos: bool,
>      no_sockets: bool,
>      exclude: Option<Vec<String>>,
> -    entries_max: isize,
> +    entries_max: i64,
>      payload_output: Option<String>,
>  ) -> Result<(), Error> {
>      let patterns = {
> diff --git a/src/api2/backup/upload_chunk.rs b/src/api2/backup/upload_chunk.rs
> index f8b7ecbd..8dd7e4d5 100644
> --- a/src/api2/backup/upload_chunk.rs
> +++ b/src/api2/backup/upload_chunk.rs
> @@ -134,10 +134,9 @@ pub const API_METHOD_UPLOAD_FIXED_CHUNK: ApiMethod = ApiMethod::new(
>                  "encoded-size",
>                  false,
>                  &IntegerSchema::new("Encoded chunk size.")
> -                    .minimum((std::mem::size_of::<DataBlobHeader>() as isize) + 1)
> +                    .minimum((std::mem::size_of::<DataBlobHeader>() as i64) + 1)
>                      .maximum(
> -                        1024 * 1024 * 16
> -                            + (std::mem::size_of::<EncryptedDataBlobHeader>() as isize)
> +                        1024 * 1024 * 16 + (std::mem::size_of::<EncryptedDataBlobHeader>() as i64)
>                      )
>                      .schema()
>              ),
> @@ -197,10 +196,9 @@ pub const API_METHOD_UPLOAD_DYNAMIC_CHUNK: ApiMethod = ApiMethod::new(
>                  "encoded-size",
>                  false,
>                  &IntegerSchema::new("Encoded chunk size.")
> -                    .minimum((std::mem::size_of::<DataBlobHeader>() as isize) + 1)
> +                    .minimum((std::mem::size_of::<DataBlobHeader>() as i64) + 1)
>                      .maximum(
> -                        1024 * 1024 * 16
> -                            + (std::mem::size_of::<EncryptedDataBlobHeader>() as isize)
> +                        1024 * 1024 * 16 + (std::mem::size_of::<EncryptedDataBlobHeader>() as i64)
>                      )
>                      .schema()
>              ),
> @@ -357,10 +355,9 @@ pub const API_METHOD_UPLOAD_BLOB: ApiMethod = ApiMethod::new(
>                  "encoded-size",
>                  false,
>                  &IntegerSchema::new("Encoded blob size.")
> -                    .minimum(std::mem::size_of::<DataBlobHeader>() as isize)
> +                    .minimum(std::mem::size_of::<DataBlobHeader>() as i64)
>                      .maximum(
> -                        1024 * 1024 * 16
> -                            + (std::mem::size_of::<EncryptedDataBlobHeader>() as isize)
> +                        1024 * 1024 * 16 + (std::mem::size_of::<EncryptedDataBlobHeader>() as i64)
>                      )
>                      .schema()
>              )
> -- 
> 2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] applied-series: [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (30 preceding siblings ...)
  2025-09-04  9:10 ` [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Dominik Csapak
@ 2025-09-04 13:27 ` Wolfgang Bumiller
  2025-09-05 12:37 ` [pdm-devel] " Hannes Duerr
  32 siblings, 0 replies; 40+ messages in thread
From: Wolfgang Bumiller @ 2025-09-04 13:27 UTC (permalink / raw)
  To: Stefan Hanreich; +Cc: pdm-devel

applied series, thanks

On Thu, Sep 04, 2025 at 10:18:25AM +0200, Stefan Hanreich wrote:
> ## Introduction
> 
> This patch series adds a new panel to the PDM that shows an overview of the
> current state of all EVPN zones across all remotes. It includes two different
> tree views:
> 
> * IP-VRFs: that shows the contents of all IP-VRFs (identified by their Route
>   Target = ASN:VNI) across all remotes.
> * Zones: that shows the contents of a specific zone on a specific remote.
> 
> For more information on the two tree views, consult the respective commits that
> introduce the components.
> 
> The panel also allows users to create new Zones / VNets on multiple remotes
> simultaneously by utilizing the new SDN locking functionality.
> 
> I have provided prebuilt packages on the share in the folder pdm-evpn.
> 
> This patch series requires the ParallelFetcher patch series from Lukas in order
> to work.
> 
> ## API
> 
> It introduces the following API endpoints on PDM:
> 
> /sdn
>     GET /controllers - list the controllers of all remotes
>     GET /zones - list the zones of all remotes
>     POST /zones - create a zone on multiple remotes
>     GET /vnets - list the vnets of all remotes
>     POST /vnets - create a vnet on multiple remotes
> 
> 
> ## Additional remarks
> 
> This patch series contains some preparatory patches that are not directly
> related to the implemented functionality:
> 
> * One fix for proxmox-schema so values that are larger than i32 can be used in
>   the integer schema definition (required for e.g. 64-bit ASNs)
> * Add JSONSchema to a lot of SDN API endpoints that were previously undocumented
> 
> I have sent them initially as separate patch series, but since they are a hard
> requirement for this patch series I have merged all of them into one patch
> series now. This way it is easier to keep track of the requirements.
> 
> 
> ## Open questions for reviewers
> 
> * The LockedSdnClient(s) are abstractions for locked SDN remotes. I'm still a
> bit unsure about the design / implementation but for future features I will be
> making more complex changes across multiple remotes so I figured an abstraction
> for this will come in handy in the future.
> 
> I'd love some inputs / opinions on the API design as well as the general concept
> of locking config -> making changes -> rolling back / applying.
> 
> I will work on a more sophisticated implementation utilizing tokio-specific
> functions in the following days, but I wanted to get the patch series out now
> and validate the API / general idea.
> 
> * We might wanna move the EvpnRouteTarget type out of the UI, even though it is
> currently only used there.
> 
> * Should we introduce a caching mechanism for the SDN API calls?
> 
> I have shortly talked about this with @Lukas, but we decided against
> implementing such a mechanism for now after some deliberation.
> 
> Showing outdated information is particularly problematic with configuration,
> especially because the create dialogues rely on that information.
> 
> After creating a new zone / vnet we would have to hit the remotes anyway, in
> order to be able to show the updated data immediately.
> 
> The downside is of course a long load time for the EVPN panel, as well as a long
> load if even one of the remotes is not available.
> 
> For an initial release I think it is fine to go forward without caching and see
> how it works out in practice based on reports from our users. Any input on this
> matter would be greatly appreciated!
> 
> 
> ## Future Work
> * show the output of the new status API calls created by Gabriel in the views.
> * add a functionality for grouping remotes together, instead of implicitly
>   grouping them based on ASN:VNI
> * introduce a caching mechanism for the SDN API calls (?)
> * integration tests with mocked SDN clients
> * add some QoL to the UI (e.g expand/collapse all)
> 
> 
> Huge thanks to @Lukas and @Dominik for helping me greatly on moving this patch
> series forward the last few days!
> 
> ## Changelog
> 
> Changes from v3 (Thanks @Shannon, @Wolfang):
> * created dedicated verification functions for SDN IDs
> * improved / fixed regex for SDN ids in the process
> * improved API documentation for SDN zones in PVE
> * use new verification functions in PDM types as well
> * SDN client prints the correct remote when releasing the lock
> 
> Changes from v2:
> * detect invalid response from rollback endpoint to gracefully handle unpatched
>   libpve-network-api-perl
> * use create_toolbar instead of implementing a whole component
> * pass is_loading to Refresh button
> * show spinner on initial load instead of empty trees
> * improved default sorting order for remotes tree
> * sort PveClients in LockedSdnClients to provide ordered output
> * use HashSet for all list endpoints for deduplication and efficient filtering
> 
> Changes from v1:
> * detect legacy PVE remotes without SDN locking API capability
> * remove already applied patch
> * parallelize list endpoints via Lukas' ParallelFetcher
> * reversed toolbar / grid order in EVPN panel
> * updated and improved commit messages
> * added missing translation macro invocations
> * replaced thread_local in components
> * store columns in component to avoid re-creating them on update
> * add better error message in add_zone/vnet dialogues if there is no
>   controller / zone
> * remove unused message from vrf/remote tree components
> * use update_root_tree for restoring tree state
> * moved EVPN above remotes in the main menu
> * added instructions on how to unlock SDN configuration in cases of errors
> 
> Changes from RFC v2:
> * rebased on top of current master
> * improved error handling for the yew components considerably
> * tinkered with column sizes in the remote view
> * preserve collapsed state on refresh
> * fix SDN ID schema definition
> * improved EVPN icon
> * moved task descriptions from yew-comp to pdm
> * improved default sorting order for the remote view
> 
> Changes from RFC v1:
> * overhauled the structure of the trees completely
>   * split the initial tree view into two distinct tree views
>   * changed the grouping of elements
>   * improved and unified the terms used across all UI elements
> * improved toolbar design
> * removed the controller data table, since the tree views should now include
>   that information
> * improved locked SDN client and added a collection type for locked SDN clients
> * improved error handling and logging considerably for the worker tasks
> 
> 
> ## Dependencies:
> pbs-api-types depends on proxmox-schema
> proxmox-api-types depends on proxmox-schema
> proxmox-backup depends on proxmox-schema
> proxmox-datacenter-manager depends on proxmox-schema
> 
> proxmox-api-types depends on pve-network
> proxmox-datacenter-manager depends on proxmox-api-types
> proxmox-datacenter-manager depends on pve-network
> 
> proxmox:
> 
> Stefan Hanreich (2):
>   schema: use i64 for minimum / maximum / default integer values
>   pbs-api-types: fix values for integer schemas
> 
>  pbs-api-types/src/datastore.rs  |  6 +++---
>  proxmox-schema/src/de/mod.rs    |  3 +--
>  proxmox-schema/src/de/verify.rs | 13 ++++++++-----
>  proxmox-schema/src/schema.rs    | 18 +++++++++---------
>  4 files changed, 21 insertions(+), 19 deletions(-)
> 
> 
> proxmox-backup:
> 
> Stefan Hanreich (1):
>   api: change integer schema parameters to i64
> 
>  pbs-tape/src/bin/pmt.rs           |  6 +++---
>  proxmox-backup-client/src/main.rs |  2 +-
>  pxar-bin/src/main.rs              |  6 +++---
>  src/api2/backup/upload_chunk.rs   | 15 ++++++---------
>  4 files changed, 13 insertions(+), 16 deletions(-)
> 
> 
> pve-network:
> 
> Stefan Hanreich (6):
>   sdn: api: return null for rollback / lock endpoints
>   controllers: fix maximum value for ASN
>   api: add state standard option
>   api: controllers: update schema of endpoints
>   api: vnets: update schema of endpoints
>   api: zones: update schema of endpoints
> 
>  src/PVE/API2/Network/SDN.pm                   |   4 +
>  src/PVE/API2/Network/SDN/Controllers.pm       | 116 +++++++++-
>  src/PVE/API2/Network/SDN/Vnets.pm             |  92 +++++++-
>  src/PVE/API2/Network/SDN/Zones.pm             | 204 ++++++++++++++++--
>  src/PVE/Network/SDN.pm                        |  10 +
>  src/PVE/Network/SDN/Controllers/BgpPlugin.pm  |   7 +-
>  src/PVE/Network/SDN/Controllers/EvpnPlugin.pm |   2 +-
>  src/PVE/Network/SDN/Controllers/IsisPlugin.pm |   6 +-
>  src/PVE/Network/SDN/VnetPlugin.pm             |  21 +-
>  src/PVE/Network/SDN/Zones/EvpnPlugin.pm       |  22 +-
>  src/PVE/Network/SDN/Zones/QinQPlugin.pm       |   6 +-
>  src/PVE/Network/SDN/Zones/VlanPlugin.pm       |   1 +
>  src/PVE/Network/SDN/Zones/VxlanPlugin.pm      |  16 +-
>  13 files changed, 459 insertions(+), 48 deletions(-)
> 
> 
> proxmox-api-types:
> 
> Stefan Hanreich (6):
>   sdn: add list/create zone endpoints
>   sdn: add list/create vnet endpoints
>   sdn: add list/create controller endpoints
>   sdn: add sdn configuration locking endpoints
>   tasks: add helper for querying successfully finished tasks
>   sdn: add helpers for pending values
> 
>  pve-api-types/generate.pl            | 38 +++++++++++++++++++
>  pve-api-types/src/lib.rs             |  1 +
>  pve-api-types/src/sdn.rs             | 33 ++++++++++++++++
>  pve-api-types/src/types/mod.rs       |  4 ++
>  pve-api-types/src/types/verifiers.rs | 56 ++++++++++++++++++++++++++++
>  5 files changed, 132 insertions(+)
>  create mode 100644 pve-api-types/src/sdn.rs
> 
> 
> proxmox-datacenter-manager:
> 
> Stefan Hanreich (15):
>   server: add locked sdn client helpers
>   ui: pve: sdn: add descriptions for sdn tasks
>   api: sdn: add list_zones endpoint
>   api: sdn: add create_zone endpoint
>   api: sdn: add list_vnets endpoint
>   api: sdn: add create_vnet endpoint
>   api: sdn: add list_controllers endpoint
>   ui: sdn: add EvpnRouteTarget type
>   ui: sdn: add vnet icon
>   ui: sdn: add view for showing evpn zones
>   ui: sdn: add view for showing ip vrfs
>   ui: sdn: add component for creating evpn vnets
>   ui: sdn: add component for creatin evpn zones
>   ui: sdn: add evpn overview panel
>   ui: sdn: add evpn panel to main menu
> 
>  lib/pdm-api-types/Cargo.toml      |   2 +
>  lib/pdm-api-types/src/lib.rs      |   2 +
>  lib/pdm-api-types/src/sdn.rs      | 171 ++++++++++
>  lib/pdm-client/src/lib.rs         |  61 ++++
>  server/src/api/mod.rs             |   2 +
>  server/src/api/sdn/controllers.rs | 114 +++++++
>  server/src/api/sdn/mod.rs         |  17 +
>  server/src/api/sdn/vnets.rs       | 180 +++++++++++
>  server/src/api/sdn/zones.rs       | 206 +++++++++++++
>  server/src/lib.rs                 |   1 +
>  server/src/sdn_client.rs          | 432 ++++++++++++++++++++++++++
>  ui/css/pdm.scss                   |  14 +-
>  ui/images/icon-sdn-vnet.svg       |   6 +
>  ui/src/lib.rs                     |   2 +
>  ui/src/main_menu.rs               |  10 +
>  ui/src/sdn/evpn/add_vnet.rs       | 313 +++++++++++++++++++
>  ui/src/sdn/evpn/add_zone.rs       | 328 ++++++++++++++++++++
>  ui/src/sdn/evpn/evpn_panel.rs     | 262 ++++++++++++++++
>  ui/src/sdn/evpn/mod.rs            |  41 +++
>  ui/src/sdn/evpn/remote_tree.rs    | 496 ++++++++++++++++++++++++++++++
>  ui/src/sdn/evpn/vrf_tree.rs       | 409 ++++++++++++++++++++++++
>  ui/src/sdn/mod.rs                 |   1 +
>  ui/src/tasks.rs                   |   4 +
>  23 files changed, 3073 insertions(+), 1 deletion(-)
>  create mode 100644 lib/pdm-api-types/src/sdn.rs
>  create mode 100644 server/src/api/sdn/controllers.rs
>  create mode 100644 server/src/api/sdn/mod.rs
>  create mode 100644 server/src/api/sdn/vnets.rs
>  create mode 100644 server/src/api/sdn/zones.rs
>  create mode 100644 server/src/sdn_client.rs
>  create mode 100644 ui/images/icon-sdn-vnet.svg
>  create mode 100644 ui/src/sdn/evpn/add_vnet.rs
>  create mode 100644 ui/src/sdn/evpn/add_zone.rs
>  create mode 100644 ui/src/sdn/evpn/evpn_panel.rs
>  create mode 100644 ui/src/sdn/evpn/mod.rs
>  create mode 100644 ui/src/sdn/evpn/remote_tree.rs
>  create mode 100644 ui/src/sdn/evpn/vrf_tree.rs
>  create mode 100644 ui/src/sdn/mod.rs
> 
> 
> Summary over all repositories:
>   49 files changed, 3698 insertions(+), 84 deletions(-)


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* Re: [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration
  2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (31 preceding siblings ...)
  2025-09-04 13:27 ` [pdm-devel] applied-series: " Wolfgang Bumiller
@ 2025-09-05 12:37 ` Hannes Duerr
  32 siblings, 0 replies; 40+ messages in thread
From: Hannes Duerr @ 2025-09-05 12:37 UTC (permalink / raw)
  To: Proxmox Datacenter Manager development discussion; +Cc: pdm-devel

better late than never, some things I noticed while testing this
series.

I tested the series with two evpn Clusters which we're not peering
each other but just coexisted.
I added them as remotes and got the overview of the existing zones and
vnets. Afterwards i removed the existing Zones in one Cluster and
added another zone and vnet via PDM.

Adding new zones works very smoothly, but it doesn't seem to be very
useful yet, as important configuration options such as route target
import and exit nodes are still missing, and you still have to
configure these options directly in the Proxmox VE cluster.
-> This is not listed as future work, but I think stefan told me
of-list that this is of course also planned to be added.
Adding vnets also works very smoothly.

The UI is also very pleasant, and you get a good overview of your
EVPN deployments, which I really like.

I will continue testing next week and I am planning on adding another
cluster to peer against an existing Cluster.
I also didn't yet manage to test the rollback mechanisms and some edge
case testing.


On Thu Sep 4, 2025 at 10:18 AM CEST, Stefan Hanreich wrote:
[...]
> ## Future Work
> * show the output of the new status API calls created by Gabriel in the views.
> * add a functionality for grouping remotes together, instead of implicitly
>   grouping them based on ASN:VNI
What about showing more information about the EVPN Controller as well?
I think it would be nice to have the peering information of the EVPN
controller shown in the UI as well, this would also make the
grouping/assignment easier.
For example, in a spine/leaf architecture where a PVE cluster is
located in a rack with ToR switches as leaves and the EVPN controller
in the cluster peers with the spine switch which could be a Route
Reflector. Here, you would see that several EVPN zones peer with the
same spine switch and therefore belong together.



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


^ permalink raw reply	[flat|nested] 40+ messages in thread

* [pdm-devel] [PATCH pve-network v4 2/6] controllers: fix maximum value for ASN
  2025-09-03 16:35 Stefan Hanreich
@ 2025-09-03 16:35 ` Stefan Hanreich
  0 siblings, 0 replies; 40+ messages in thread
From: Stefan Hanreich @ 2025-09-03 16:35 UTC (permalink / raw)
  To: pdm-devel

ASNs are 32-bit unsigned integers where the maximum value is
4294967295.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
index 021673b..e53000a 100644
--- a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
@@ -25,7 +25,7 @@ sub properties {
             type => 'integer',
             description => "autonomous system number",
             minimum => 0,
-            maximum => 4294967296,
+            maximum => 2**32 - 1,
         },
         fabric => {
             description => "SDN fabric to use as underlay for this EVPN controller.",
-- 
2.47.2


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


^ permalink raw reply	[flat|nested] 40+ messages in thread

end of thread, other threads:[~2025-09-05 12:37 UTC | newest]

Thread overview: 40+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-09-04  8:18 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Stefan Hanreich
2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox v4 1/2] schema: use i64 for minimum / maximum / default integer values Stefan Hanreich
2025-09-04 10:03   ` [pdm-devel] applied: " Wolfgang Bumiller
2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox v4 2/2] pbs-api-types: fix values for integer schemas Stefan Hanreich
2025-09-04 10:03   ` [pdm-devel] applied: " Wolfgang Bumiller
2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-backup v4 1/1] api: change integer schema parameters to i64 Stefan Hanreich
2025-09-04 12:46   ` [pdm-devel] applied: " Wolfgang Bumiller
2025-09-04  8:18 ` [pdm-devel] [PATCH pve-network v4 1/6] sdn: api: return null for rollback / lock endpoints Stefan Hanreich
2025-09-04 12:31   ` [pdm-devel] appled: " Wolfgang Bumiller
2025-09-04  8:18 ` [pdm-devel] [PATCH pve-network v4 2/6] controllers: fix maximum value for ASN Stefan Hanreich
2025-09-04  8:18 ` [pdm-devel] [PATCH pve-network v4 3/6] api: add state standard option Stefan Hanreich
2025-09-04  8:18 ` [pdm-devel] [PATCH pve-network v4 4/6] api: controllers: update schema of endpoints Stefan Hanreich
2025-09-04  8:18 ` [pdm-devel] [PATCH pve-network v4 5/6] api: vnets: " Stefan Hanreich
2025-09-04  8:18 ` [pdm-devel] [PATCH pve-network v4 6/6] api: zones: " Stefan Hanreich
2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-api-types v4 1/6] sdn: add list/create zone endpoints Stefan Hanreich
2025-09-04 12:42   ` [pdm-devel] applied: " Wolfgang Bumiller
2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-api-types v4 2/6] sdn: add list/create vnet endpoints Stefan Hanreich
2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-api-types v4 3/6] sdn: add list/create controller endpoints Stefan Hanreich
2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-api-types v4 4/6] sdn: add sdn configuration locking endpoints Stefan Hanreich
2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-api-types v4 5/6] tasks: add helper for querying successfully finished tasks Stefan Hanreich
2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-api-types v4 6/6] sdn: add helpers for pending values Stefan Hanreich
2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 01/15] server: add locked sdn client helpers Stefan Hanreich
2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 02/15] ui: pve: sdn: add descriptions for sdn tasks Stefan Hanreich
2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 03/15] api: sdn: add list_zones endpoint Stefan Hanreich
2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 04/15] api: sdn: add create_zone endpoint Stefan Hanreich
2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 05/15] api: sdn: add list_vnets endpoint Stefan Hanreich
2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 06/15] api: sdn: add create_vnet endpoint Stefan Hanreich
2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 07/15] api: sdn: add list_controllers endpoint Stefan Hanreich
2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 08/15] ui: sdn: add EvpnRouteTarget type Stefan Hanreich
2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 09/15] ui: sdn: add vnet icon Stefan Hanreich
2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 10/15] ui: sdn: add view for showing evpn zones Stefan Hanreich
2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 11/15] ui: sdn: add view for showing ip vrfs Stefan Hanreich
2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 12/15] ui: sdn: add component for creating evpn vnets Stefan Hanreich
2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 13/15] ui: sdn: add component for creatin evpn zones Stefan Hanreich
2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 14/15] ui: sdn: add evpn overview panel Stefan Hanreich
2025-09-04  8:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager v4 15/15] ui: sdn: add evpn panel to main menu Stefan Hanreich
2025-09-04  9:10 ` [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v4 00/30] Add initial SDN / EVPN integration Dominik Csapak
2025-09-04 13:27 ` [pdm-devel] applied-series: " Wolfgang Bumiller
2025-09-05 12:37 ` [pdm-devel] " Hannes Duerr
  -- strict thread matches above, loose matches on Subject: below --
2025-09-03 16:35 Stefan Hanreich
2025-09-03 16:35 ` [pdm-devel] [PATCH pve-network v4 2/6] controllers: fix maximum value for ASN 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