all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration
@ 2025-08-22 13:49 Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox v2 1/2] schema: use i64 for minimum / maximum / default integer values Stefan Hanreich
                   ` (34 more replies)
  0 siblings, 35 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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


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


## Changelog

Changes since the 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-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 proxmox-yew-comp

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             | 203 ++++++++++++++++--
 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      |  15 +-
 13 files changed, 457 insertions(+), 48 deletions(-)


proxmox-api-types:

Stefan Hanreich (7):
  add QemuMigratePreconditionsNotAllowedNodesBlockingHaResources struct
  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      | 40 ++++++++++++++++++++++++++++++++++
 pve-api-types/src/lib.rs       |  1 +
 pve-api-types/src/sdn.rs       | 33 ++++++++++++++++++++++++++++
 pve-api-types/src/types/mod.rs |  4 ++++
 4 files changed, 78 insertions(+)
 create mode 100644 pve-api-types/src/sdn.rs


proxmox-yew-comp:

Stefan Hanreich (1):
  sdn: add descriptions for sdn tasks

 src/utils.rs | 3 +++
 1 file changed, 3 insertions(+)


proxmox-datacenter-manager:

Stefan Hanreich (15):
  server: add locked sdn client helpers
  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 remote tree component
  ui: add view for showing ip vrfs
  ui: sdn: add AddVnetWindow component
  ui: sdn: add AddZoneWindow component
  ui: sdn: add EvpnPanel
  ui: sdn: add EvpnPanel to main menu
  pve: sdn: add descriptions for sdn tasks

 lib/pdm-api-types/Cargo.toml      |   2 +
 lib/pdm-api-types/src/lib.rs      |   2 +
 lib/pdm-api-types/src/sdn.rs      | 168 +++++++++++++
 lib/pdm-client/src/lib.rs         |  61 +++++
 server/src/api/mod.rs             |   2 +
 server/src/api/sdn/controllers.rs |  78 ++++++
 server/src/api/sdn/mod.rs         |  17 ++
 server/src/api/sdn/vnets.rs       | 149 +++++++++++
 server/src/api/sdn/zones.rs       | 174 +++++++++++++
 server/src/lib.rs                 |   1 +
 server/src/sdn_client.rs          | 387 ++++++++++++++++++++++++++++
 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       | 273 ++++++++++++++++++++
 ui/src/sdn/evpn/add_zone.rs       | 281 +++++++++++++++++++++
 ui/src/sdn/evpn/evpn_panel.rs     | 224 +++++++++++++++++
 ui/src/sdn/evpn/mod.rs            |  41 +++
 ui/src/sdn/evpn/remote_tree.rs    | 403 ++++++++++++++++++++++++++++++
 ui/src/sdn/evpn/vrf_tree.rs       | 345 +++++++++++++++++++++++++
 ui/src/sdn/mod.rs                 |   1 +
 ui/src/tasks.rs                   |   2 +
 23 files changed, 2642 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, 3214 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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox v2 1/2] schema: use i64 for minimum / maximum / default integer values
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox v2 2/2] pbs-api-types: fix values for integer schemas Stefan Hanreich
                   ` (33 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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 ddbbacd4..8a99909f 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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox v2 2/2] pbs-api-types: fix values for integer schemas
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox v2 1/2] schema: use i64 for minimum / maximum / default integer values Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-backup v2 1/1] api: change integer schema parameters to i64 Stefan Hanreich
                   ` (32 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox-backup v2 1/1] api: change integer schema parameters to i64
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox v2 1/2] schema: use i64 for minimum / maximum / default integer values Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox v2 2/2] pbs-api-types: fix values for integer schemas Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH pve-network v2 1/6] sdn: api: return null for rollback / lock endpoints Stefan Hanreich
                   ` (31 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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 35378377..574f839b 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] 43+ messages in thread

* [pdm-devel] [PATCH pve-network v2 1/6] sdn: api: return null for rollback / lock endpoints
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (2 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-backup v2 1/1] api: change integer schema parameters to i64 Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH pve-network v2 2/6] controllers: fix maximum value for ASN Stefan Hanreich
                   ` (30 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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] 43+ messages in thread

* [pdm-devel] [PATCH pve-network v2 2/6] controllers: fix maximum value for ASN
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (3 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH pve-network v2 1/6] sdn: api: return null for rollback / lock endpoints Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH pve-network v2 3/6] api: add state standard option Stefan Hanreich
                   ` (29 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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] 43+ messages in thread

* [pdm-devel] [PATCH pve-network v2 3/6] api: add state standard option
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (4 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH pve-network v2 2/6] controllers: fix maximum value for ASN Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH pve-network v2 4/6] api: controllers: update schema of endpoints Stefan Hanreich
                   ` (28 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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] 43+ messages in thread

* [pdm-devel] [PATCH pve-network v2 4/6] api: controllers: update schema of endpoints
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (5 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH pve-network v2 3/6] api: add state standard option Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH pve-network v2 5/6] api: vnets: " Stefan Hanreich
                   ` (27 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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] 43+ messages in thread

* [pdm-devel] [PATCH pve-network v2 5/6] api: vnets: update schema of endpoints
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (6 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH pve-network v2 4/6] api: controllers: update schema of endpoints Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH pve-network v2 6/6] api: zones: " Stefan Hanreich
                   ` (26 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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] 43+ messages in thread

* [pdm-devel] [PATCH pve-network v2 6/6] api: zones: update schema of endpoints
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (7 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH pve-network v2 5/6] api: vnets: " Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-api-types v2 1/7] add QemuMigratePreconditionsNotAllowedNodesBlockingHaResources struct Stefan Hanreich
                   ` (25 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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        | 203 +++++++++++++++++++++--
 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 |  15 +-
 5 files changed, 218 insertions(+), 29 deletions(-)

diff --git a/src/PVE/API2/Network/SDN/Zones.pm b/src/PVE/API2/Network/SDN/Zones.pm
index 0e4726b..cc51652 100644
--- a/src/PVE/API2/Network/SDN/Zones.pm
+++ b/src/PVE/API2/Network/SDN/Zones.pm
@@ -62,6 +62,147 @@ 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,
+    },
+    '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 +239,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 +327,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..4354bff 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..3c72d35 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..9d6932f 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..7ab89da 100644
--- a/src/PVE/Network/SDN/Zones/VxlanPlugin.pm
+++ b/src/PVE/Network/SDN/Zones/VxlanPlugin.pm
@@ -27,21 +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',
         },
+        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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox-api-types v2 1/7] add QemuMigratePreconditionsNotAllowedNodesBlockingHaResources struct
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (8 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH pve-network v2 6/6] api: zones: " Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-api-types v2 2/7] sdn: add list/create zone endpoints Stefan Hanreich
                   ` (24 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 UTC (permalink / raw)
  To: pdm-devel

required for the generated rust code to compile succesfully with the
API definitions from PVE 9.

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

diff --git a/pve-api-types/generate.pl b/pve-api-types/generate.pl
index b761a88..64f1555 100644
--- a/pve-api-types/generate.pl
+++ b/pve-api-types/generate.pl
@@ -301,6 +301,8 @@ Schema2Rust::derive('ListRealm' => 'Clone', 'PartialEq');
 api(POST => '/access/users/{userid}/token/{tokenid}', 'create_token', 'param-name' => 'CreateToken');
 Schema2Rust::derive('CreateToken' => 'Default');
 
+Schema2Rust::derive('QemuMigratePreconditionsNotAllowedNodesBlockingHaResources' => '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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox-api-types v2 2/7] sdn: add list/create zone endpoints
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (9 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-api-types v2 1/7] add QemuMigratePreconditionsNotAllowedNodesBlockingHaResources struct Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-api-types v2 3/7] sdn: add list/create vnet endpoints Stefan Hanreich
                   ` (23 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 UTC (permalink / raw)
  To: pdm-devel

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

diff --git a/pve-api-types/generate.pl b/pve-api-types/generate.pl
index 64f1555..617527d 100644
--- a/pve-api-types/generate.pl
+++ b/pve-api-types/generate.pl
@@ -84,6 +84,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' => { regex => '^[a-z][a-z0-9]*[a-z0-9]$' });
+
 Schema2Rust::register_enum_variant('PveVmCpuConfReportedModel::486' => 'I486');
 Schema2Rust::register_enum_variant('QemuConfigEfidisk0Efitype::2m' => 'Mb2');
 Schema2Rust::register_enum_variant('QemuConfigEfidisk0Efitype::4m' => 'Mb4');
@@ -109,6 +111,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' => { regex => '^(\d+):(\d+)$' });
+Schema2Rust::register_format('pve-sdn-controller-id' => { regex => '^[a-z][a-z0-9_-]*[a-z0-9]$' });
+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',
@@ -301,6 +307,18 @@ Schema2Rust::derive('ListRealm' => 'Clone', 'PartialEq');
 api(POST => '/access/users/{userid}/token/{tokenid}', 'create_token', 'param-name' => 'CreateToken');
 Schema2Rust::derive('CreateToken' => 'Default');
 
+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');
+
 Schema2Rust::derive('QemuMigratePreconditionsNotAllowedNodesBlockingHaResources' => 'Clone', 'PartialEq');
 
 # NOW DUMP THE CODE:
-- 
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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox-api-types v2 3/7] sdn: add list/create vnet endpoints
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (10 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-api-types v2 2/7] sdn: add list/create zone endpoints Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-api-types v2 4/7] sdn: add list/create controller endpoints Stefan Hanreich
                   ` (22 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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 617527d..34020ee 100644
--- a/pve-api-types/generate.pl
+++ b/pve-api-types/generate.pl
@@ -85,6 +85,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' => { regex => '^[a-z][a-z0-9]*[a-z0-9]$' });
+Schema2Rust::register_format('pve-sdn-vnet-id' => { regex => '^[a-z][a-z0-9]*[a-z0-9]$' });
 
 Schema2Rust::register_enum_variant('PveVmCpuConfReportedModel::486' => 'I486');
 Schema2Rust::register_enum_variant('QemuConfigEfidisk0Efitype::2m' => 'Mb2');
@@ -319,6 +320,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');
+
 Schema2Rust::derive('QemuMigratePreconditionsNotAllowedNodesBlockingHaResources' => 'Clone', 'PartialEq');
 
 # NOW DUMP THE CODE:
-- 
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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox-api-types v2 4/7] sdn: add list/create controller endpoints
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (11 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-api-types v2 3/7] sdn: add list/create vnet endpoints Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-api-types v2 5/7] sdn: add sdn configuration locking endpoints Stefan Hanreich
                   ` (21 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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 34020ee..3f21ca0 100644
--- a/pve-api-types/generate.pl
+++ b/pve-api-types/generate.pl
@@ -115,6 +115,7 @@ Schema2Rust::register_format('pve-vlan-id-or-range' => { code => 'verifiers::ver
 Schema2Rust::register_format('pve-sdn-bgp-rt' => { regex => '^(\d+):(\d+)$' });
 Schema2Rust::register_format('pve-sdn-controller-id' => { regex => '^[a-z][a-z0-9_-]*[a-z0-9]$' });
 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' => { regex => '^[a-zA-Z0-9][a-zA-Z0-9-]{0,6}[a-zA-Z0-9]?$' });
 
 # This is used as both a task status and guest status.
 Schema2Rust::generate_enum('IsRunning', {
@@ -320,6 +321,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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox-api-types v2 5/7] sdn: add sdn configuration locking endpoints
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (12 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-api-types v2 4/7] sdn: add list/create controller endpoints Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-api-types v2 6/7] tasks: add helper for querying successfully finished tasks Stefan Hanreich
                   ` (20 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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 3f21ca0..443153c 100644
--- a/pve-api-types/generate.pl
+++ b/pve-api-types/generate.pl
@@ -333,6 +333,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');
+
 Schema2Rust::derive('QemuMigratePreconditionsNotAllowedNodesBlockingHaResources' => 'Clone', 'PartialEq');
 
 # NOW DUMP THE CODE:
-- 
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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox-api-types v2 6/7] tasks: add helper for querying successfully finished tasks
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (13 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-api-types v2 5/7] sdn: add sdn configuration locking endpoints Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-api-types v2 7/7] sdn: add helpers for pending values Stefan Hanreich
                   ` (19 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox-api-types v2 7/7] sdn: add helpers for pending values
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (14 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-api-types v2 6/7] tasks: add helper for querying successfully finished tasks Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-yew-comp v2 1/1] sdn: add descriptions for sdn tasks Stefan Hanreich
                   ` (18 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox-yew-comp v2 1/1] sdn: add descriptions for sdn tasks
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (15 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-api-types v2 7/7] sdn: add helpers for pending values Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-26 13:22   ` Dominik Csapak
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 01/15] server: add locked sdn client helpers Stefan Hanreich
                   ` (17 subsequent siblings)
  34 siblings, 1 reply; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 UTC (permalink / raw)
  To: pdm-devel

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

diff --git a/src/utils.rs b/src/utils.rs
index 1a4ad40..3d5def1 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -225,6 +225,9 @@ pub fn init_task_descr_table_base() {
     register_task_description("srvstop", (tr!("Service"), tr!("Stop")));
     register_task_description("srvrestart", (tr!("Service"), tr!("Restart")));
     register_task_description("srvreload", (tr!("Service"), tr!("Reload")));
+
+    register_task_description("create_zone", tr!("Create EVPN VRF (Zone)"));
+    register_task_description("create_vnet", tr!("Create EVPN VNet"));
 }
 
 /// Uses information from the given [`UPID`] to render the task description with [`format_task_description`]
-- 
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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v2 01/15] server: add locked sdn client helpers
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (16 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-yew-comp v2 1/1] sdn: add descriptions for sdn tasks Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 02/15] api: sdn: add list_zones endpoint Stefan Hanreich
                   ` (16 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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 and rollbacking the configuration or
relea and rollbacking the configuration or releasing the locks.

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 the
remotes if any API command on any remote fails. This client will be
used for making changes across all remotes in PDM worker tasks.

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

diff --git a/server/src/lib.rs b/server/src/lib.rs
index 485bc79..760de7d 100644
--- a/server/src/lib.rs
+++ b/server/src/lib.rs
@@ -13,6 +13,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..b957bdb
--- /dev/null
+++ b/server/src/sdn_client.rs
@@ -0,0 +1,387 @@
+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 {
+    secret: String,
+    client: Arc<dyn PveClient + Send + Sync>,
+}
+
+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, anyhow::Error> {
+        let client = connect(remote)?;
+
+        let params = CreateSdnLock {
+            allow_pending: allow_pending.into(),
+        };
+
+        let secret = client.acquire_sdn_lock(params).await?;
+
+        Ok(Self { secret, client })
+    }
+
+    /// 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.secret.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.secret.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.secret.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.secret),
+        };
+
+        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.secret),
+            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, _) in clients {
+                        proxmox_log::info!("releasing lock for remote {}", remote.id);
+
+                        if let Err(error) = client.release(false).await {
+                            proxmox_log::error!(
+                                "could not release lock for remote {}: {error:#}",
+                                remote.id
+                            )
+                        }
+                    }
+
+                    return Err(error).with_context(|| {
+                        format!("could not lock sdn configuration for remote {}", 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 {
+                    Ok(ctx) => {
+                        proxmox_log::info!(
+                            "successfully rolled back configuration for remote {}",
+                            ctx.remote_id()
+                        )
+                    }
+                    Err((_, ctx)) => {
+                        proxmox_log::error!(
+                            "could not rollback configuration for remote {}",
+                            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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v2 02/15] api: sdn: add list_zones endpoint
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (17 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 01/15] server: add locked sdn client helpers Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 03/15] api: sdn: add create_zone endpoint Stefan Hanreich
                   ` (15 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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  | 78 ++++++++++++++++++++++++++++++++++++
 7 files changed, 139 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 7e72a19..51758c1 100644
--- a/lib/pdm-api-types/Cargo.toml
+++ b/lib/pdm-api-types/Cargo.toml
@@ -22,3 +22,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 9373725..327e472 100644
--- a/lib/pdm-api-types/src/lib.rs
+++ b/lib/pdm-api-types/src/lib.rs
@@ -96,6 +96,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 61a8ebd..34197f7 100644
--- a/lib/pdm-client/src/lib.rs
+++ b/lib/pdm-client/src/lib.rs
@@ -7,6 +7,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};
@@ -56,6 +57,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);
@@ -865,6 +868,21 @@ impl<T: HttpApiClient> PdmClient<T> {
         .build();
         Ok(self.0.get(&path).await?.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 6c4831b..21baede 100644
--- a/server/src/api/mod.rs
+++ b/server/src/api/mod.rs
@@ -16,6 +16,7 @@ pub mod remote_tasks;
 pub mod remotes;
 pub mod resources;
 mod rrd_common;
+pub mod sdn;
 
 #[sortable]
 const SUBDIRS: SubdirMap = &sorted!([
@@ -28,6 +29,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..4b08736
--- /dev/null
+++ b/server/src/api/sdn/zones.rs
@@ -0,0 +1,78 @@
+use anyhow::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::{connect, get_remote};
+
+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<Vec<String>>,
+) -> Result<Vec<ListZone>, Error> {
+    let (remote_config, _) = pdm_config::remotes::config()?;
+
+    let remote_ids = remotes.unwrap_or_else(|| {
+        remote_config
+            .values()
+            .filter(|remote| (remote.ty == RemoteType::Pve))
+            .map(|remote| remote.id.clone())
+            .collect()
+    });
+
+    let mut result = Vec::new();
+
+    for remote in remote_ids {
+        let client = connect(get_remote(&remote_config, &remote)?)?;
+
+        for zone in client.list_zones(pending, running, ty).await? {
+            result.push(ListZone {
+                remote: remote.clone(),
+                zone,
+            })
+        }
+    }
+
+    Ok(result)
+}
-- 
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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v2 03/15] api: sdn: add create_zone endpoint
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (18 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 02/15] api: sdn: add list_zones endpoint Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 04/15] api: sdn: add list_vnets endpoint Stefan Hanreich
                   ` (14 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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 |  61 ++++++++++++++++++++
 lib/pdm-client/src/lib.rs    |   7 +++
 server/src/api/sdn/zones.rs  | 108 +++++++++++++++++++++++++++++++++--
 3 files changed, 170 insertions(+), 6 deletions(-)

diff --git a/lib/pdm-api-types/src/sdn.rs b/lib/pdm-api-types/src/sdn.rs
index 28b20c5..2f72bca 100644
--- a/lib/pdm-api-types/src/sdn.rs
+++ b/lib/pdm-api-types/src/sdn.rs
@@ -4,6 +4,67 @@ 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();
+
+const_regex! {
+    SDN_ID_FORMAT = "[a-zA-Z][a-zA-Z]{0,7}";
+}
+
+pub const SDN_ID_SCHEMA: Schema = StringSchema::new("The name for an SDN object.")
+    .min_length(1)
+    .max_length(8)
+    .format(&ApiStringFormat::Pattern(&SDN_ID_FORMAT))
+    .schema();
+
+#[api(
+    properties: {
+        remote: {
+            schema: REMOTE_ID_SCHEMA,
+        },
+        controller: {
+            schema: SDN_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 34197f7..0fbb45c 100644
--- a/lib/pdm-client/src/lib.rs
+++ b/lib/pdm-client/src/lib.rs
@@ -58,6 +58,7 @@ pub mod types {
 
     pub use pve_api_types::PveUpid;
 
+    pub use pdm_api_types::sdn::{CreateZoneParams, ListZone};
     pub use pve_api_types::ListZonesType;
 }
 
@@ -883,6 +884,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 4b08736..a0227d3 100644
--- a/server/src/api/sdn/zones.rs
+++ b/server/src/api/sdn/zones.rs
@@ -1,13 +1,23 @@
-use anyhow::Error;
+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::{connect, get_remote};
+use crate::{
+    api::pve::{connect, get_remote},
+    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: {
@@ -76,3 +86,89 @@ pub async fn list_zones(
 
     Ok(result)
 }
+
+#[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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v2 04/15] api: sdn: add list_vnets endpoint
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (19 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 03/15] api: sdn: add create_zone endpoint Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 05/15] api: sdn: add create_vnet endpoint Stefan Hanreich
                   ` (13 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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  | 72 ++++++++++++++++++++++++++++++++++++
 4 files changed, 108 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 2f72bca..e944d60 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;
@@ -65,6 +65,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)]
+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 0fbb45c..72afb3c 100644
--- a/lib/pdm-client/src/lib.rs
+++ b/lib/pdm-client/src/lib.rs
@@ -7,7 +7,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};
@@ -885,6 +885,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..2ac2956
--- /dev/null
+++ b/server/src/api/sdn/vnets.rs
@@ -0,0 +1,72 @@
+use anyhow::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::{connect, get_remote};
+
+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<Vec<String>>,
+) -> Result<Vec<ListVnet>, Error> {
+    let (remote_config, _) = pdm_config::remotes::config()?;
+
+    let remote_ids = remotes.unwrap_or_else(|| {
+        remote_config
+            .values()
+            .filter(|remote| (remote.ty == RemoteType::Pve))
+            .map(|remote| remote.id.clone())
+            .collect()
+    });
+
+    let mut result = Vec::new();
+
+    for remote in remote_ids {
+        let client = connect(get_remote(&remote_config, &remote)?)?;
+
+        for vnet in client.list_vnets(pending, running).await? {
+            result.push(ListVnet {
+                remote: remote.clone(),
+                vnet,
+            })
+        }
+    }
+
+    Ok(result)
+}
-- 
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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v2 05/15] api: sdn: add create_vnet endpoint
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (20 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 04/15] api: sdn: add list_vnets endpoint Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 06/15] api: sdn: add list_controllers endpoint Stefan Hanreich
                   ` (12 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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  | 87 +++++++++++++++++++++++++++++++++---
 3 files changed, 134 insertions(+), 6 deletions(-)

diff --git a/lib/pdm-api-types/src/sdn.rs b/lib/pdm-api-types/src/sdn.rs
index e944d60..5aaeb49 100644
--- a/lib/pdm-api-types/src/sdn.rs
+++ b/lib/pdm-api-types/src/sdn.rs
@@ -65,6 +65,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 72afb3c..25885cd 100644
--- a/lib/pdm-client/src/lib.rs
+++ b/lib/pdm-client/src/lib.rs
@@ -58,7 +58,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;
 }
 
@@ -903,6 +903,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 2ac2956..d74d461 100644
--- a/server/src/api/sdn/vnets.rs
+++ b/server/src/api/sdn/vnets.rs
@@ -1,12 +1,23 @@
-use anyhow::Error;
+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::{connect, get_remote};
+use crate::{
+    api::pve::{connect, get_remote},
+    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: {
@@ -70,3 +81,69 @@ async fn list_vnets(
 
     Ok(result)
 }
+
+#[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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v2 06/15] api: sdn: add list_controllers endpoint
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (21 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 05/15] api: sdn: add create_vnet endpoint Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 07/15] ui: sdn: add EvpnRouteTarget type Stefan Hanreich
                   ` (11 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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      | 21 ++++++++-
 lib/pdm-client/src/lib.rs         | 21 ++++++++-
 server/src/api/sdn/controllers.rs | 78 +++++++++++++++++++++++++++++++
 server/src/api/sdn/mod.rs         |  2 +
 4 files changed, 119 insertions(+), 3 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 5aaeb49..4f6df48 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 pve_api_types::{SdnController, SdnVnet, SdnZone};
 use serde::{Deserialize, Serialize};
 
 use crate::remotes::REMOTE_ID_SCHEMA;
@@ -110,6 +110,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 25885cd..1e4cace 100644
--- a/lib/pdm-client/src/lib.rs
+++ b/lib/pdm-client/src/lib.rs
@@ -58,8 +58,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);
@@ -870,6 +872,21 @@ impl<T: HttpApiClient> PdmClient<T> {
         Ok(self.0.get(&path).await?.expect_json()?.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..3a3ee5c
--- /dev/null
+++ b/server/src/api/sdn/controllers.rs
@@ -0,0 +1,78 @@
+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::{connect, get_remote};
+
+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<Vec<String>>,
+) -> Result<Vec<ListController>, Error> {
+    let (remote_config, _) = pdm_config::remotes::config()?;
+
+    let remote_ids = remotes.unwrap_or_else(|| {
+        remote_config
+            .values()
+            .filter(|remote| (remote.ty == RemoteType::Pve))
+            .map(|remote| remote.id.clone())
+            .collect()
+    });
+
+    let mut result = Vec::new();
+
+    for remote in remote_ids {
+        let client = connect(get_remote(&remote_config, &remote)?)?;
+
+        for controller in client.list_controllers(pending, running, ty).await? {
+            result.push(ListController {
+                remote: remote.clone(),
+                controller,
+            })
+        }
+    }
+
+    Ok(result)
+}
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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v2 07/15] ui: sdn: add EvpnRouteTarget type
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (22 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 06/15] api: sdn: add list_controllers endpoint Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 08/15] ui: sdn: add vnet icon Stefan Hanreich
                   ` (10 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v2 08/15] ui: sdn: add vnet icon
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (23 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 07/15] ui: sdn: add EvpnRouteTarget type Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 09/15] ui: sdn: add remote tree component Stefan Hanreich
                   ` (9 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v2 09/15] ui: sdn: add remote tree component
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (24 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 08/15] ui: sdn: add vnet icon Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 10/15] ui: add view for showing ip vrfs Stefan Hanreich
                   ` (8 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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 | 403 +++++++++++++++++++++++++++++++++
 2 files changed, 406 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..e4b0fe4
--- /dev/null
+++ b/ui/src/sdn/evpn/remote_tree.rs
@@ -0,0 +1,403 @@
+use std::cmp::Ordering;
+use std::collections::HashSet;
+use std::rc::Rc;
+use std::str::FromStr;
+
+use anyhow::Error;
+use pwt::widget::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,
+        })
+    }
+}
+
+pub enum RemoteTreeMsg {
+    SelectionChange,
+}
+
+#[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 zones_to_vrf_view(
+    controllers: &[ListController],
+    zones: &[ListZone],
+    vnets: &[ListVnet],
+) -> SlabTree<RemoteTreeEntry> {
+    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 controller = controllers
+            .iter()
+            .find(|controller| {
+                controller.remote == zone.remote
+                    && zone_data
+                        .controller
+                        .as_ref()
+                        .expect("EVPN zone has a controller")
+                        == &controller.controller.controller
+            })
+            .expect("controller of EVPN zone exists");
+
+        let route_target = EvpnRouteTarget {
+            asn: controller
+                .controller
+                .asn
+                .expect("EVPN controller has an ASN"),
+            vni: zone.zone.vrf_vxlan.expect("EVPN zone has a VXLAN ID"),
+        };
+
+        let import_targets = zone_data
+            .rt_import
+            .iter()
+            .flat_map(|rt_import| rt_import.split(',').map(EvpnRouteTarget::from_str))
+            .collect::<Result<_, Error>>()
+            .expect("rt_import contains valid route targets");
+
+        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 zone = zones
+            .iter()
+            .find(|zone| {
+                zone.remote == vnet.remote
+                    && vnet_data.zone.as_ref().expect("vnet has zone") == &zone.zone.zone
+            })
+            .expect("vnet has zone");
+
+        let controller = controllers
+            .iter()
+            .find(|controller| {
+                controller.remote == zone.remote
+                    && zone
+                        .zone
+                        .controller
+                        .as_ref()
+                        .expect("EVPN zone has a controller")
+                        == &controller.controller.controller
+            })
+            .expect("controller of EVPN zone exists");
+
+        let zone_target = EvpnRouteTarget {
+            asn: controller.controller.asn.expect("EVPN controller has ASN"),
+            vni: zone.zone.vrf_vxlan.expect("EVPN zone has a VRF VNI"),
+        };
+
+        let vnet_target = EvpnRouteTarget {
+            asn: controller.controller.asn.expect("EVPN controller has ASN"),
+            vni: vnet_data.tag.expect("EVPN vnet has a VNI"),
+        };
+
+        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,
+                    }));
+                }
+            }
+        }
+    }
+
+    tree
+}
+pub struct RemoteTreeComponent {
+    store: TreeStore<RemoteTreeEntry>,
+    selection: Selection,
+}
+
+fn name_remote_sorter(a: &RemoteTreeEntry, b: &RemoteTreeEntry) -> Ordering {
+    (a.name(), a.remote()).cmp(&(b.name(), b.remote()))
+}
+
+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()
+                })
+                .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(),
+                    _ => "",
+                })
+                .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()))
+                .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()))
+                .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(),
+                })
+                .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(),
+                })
+                .into(),
+        ])
+    }
+}
+
+impl Component for RemoteTreeComponent {
+    type Properties = RemoteTree;
+    type Message = RemoteTreeMsg;
+
+    fn create(ctx: &Context<Self>) -> Self {
+        let store = TreeStore::new().view_root(false);
+
+        let data = zones_to_vrf_view(
+            &ctx.props().controllers,
+            &ctx.props().zones,
+            &ctx.props().vnets,
+        );
+
+        store.set_data(data);
+        store.set_sorter(name_remote_sorter);
+
+        let selection =
+            Selection::new().on_select(ctx.link().callback(|_| Self::Message::SelectionChange));
+
+        Self { store, selection }
+    }
+
+    fn view(&self, _ctx: &Context<Self>) -> Html {
+        let columns = Self::columns(self.store.clone());
+
+        let table = DataTable::new(columns, 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);
+
+        Column::new()
+            .class(pwt::css::FlexFit)
+            .with_child(table)
+            .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)
+        {
+            let data = zones_to_vrf_view(
+                &ctx.props().controllers,
+                &ctx.props().zones,
+                &ctx.props().vnets,
+            );
+
+            self.store.set_data(data);
+            self.store.set_sorter(name_remote_sorter);
+
+            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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v2 10/15] ui: add view for showing ip vrfs
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (25 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 09/15] ui: sdn: add remote tree component Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 11/15] ui: sdn: add AddVnetWindow component Stefan Hanreich
                   ` (7 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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 | 345 ++++++++++++++++++++++++++++++++++++
 ui/src/sdn/mod.rs           |   1 +
 4 files changed, 351 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..8b01b00
--- /dev/null
+++ b/ui/src/sdn/evpn/vrf_tree.rs
@@ -0,0 +1,345 @@
+use std::cmp::Ordering;
+use std::collections::HashSet;
+use std::rc::Rc;
+
+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::{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,
+        })
+    }
+}
+
+pub enum VrfTreeMsg {
+    SelectionChange,
+}
+
+#[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],
+) -> SlabTree<VrfTreeEntry> {
+    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 controller = controllers
+            .iter()
+            .find(|controller| {
+                controller.remote == zone.remote
+                    && zone_data
+                        .controller
+                        .as_ref()
+                        .expect("EVPN zone has a controller")
+                        == &controller.controller.controller
+            })
+            .expect("controller of EVPN zone exists");
+
+        let route_target = EvpnRouteTarget {
+            asn: controller
+                .controller
+                .asn
+                .expect("EVPN controller as an ASN"),
+            vni: zone.zone.vrf_vxlan.expect("EVPN zone has a VXLAN ID"),
+        };
+
+        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 zone = zones
+            .iter()
+            .find(|zone| {
+                zone.remote == vnet.remote
+                    && vnet_data.zone.as_ref().expect("vnet has zone") == &zone.zone.zone
+            })
+            .expect("zone of vnet exists");
+
+        let controller = controllers
+            .iter()
+            .find(|controller| {
+                controller.remote == zone.remote
+                    && zone
+                        .zone
+                        .controller
+                        .as_ref()
+                        .expect("EVPN zone has a controller")
+                        == &controller.controller.controller
+            })
+            .expect("controller of EVPN zone exists");
+
+        let zone_target = EvpnRouteTarget {
+            asn: controller.controller.asn.expect("EVPN controller has ASN"),
+            vni: zone.zone.vrf_vxlan.expect("EVPN zone has a VRF VNI"),
+        };
+
+        let vnet_target = EvpnRouteTarget {
+            asn: controller.controller.asn.expect("EVPN controller has ASN"),
+            vni: vnet.vnet.tag.expect("EVPN vnet has a VNI"),
+        };
+
+        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
+                };
+
+                fdb_entry.append(VrfTreeEntry::Remote(RemoteData {
+                    remote: vnet.remote.clone(),
+                    zone: vnet.vnet.zone.clone().expect("vnet has a zone"),
+                    vnet: vnet.vnet.vnet.clone(),
+                }));
+            }
+        }
+    }
+
+    tree
+}
+
+pub struct VrfTreeComponent {
+    store: TreeStore<VrfTreeEntry>,
+    selection: Selection,
+}
+
+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 = VrfTreeMsg;
+
+    fn create(ctx: &Context<Self>) -> Self {
+        let store = TreeStore::new().view_root(false);
+
+        let data = zones_to_vrf_view(
+            &ctx.props().controllers,
+            &ctx.props().zones,
+            &ctx.props().vnets,
+        );
+
+        store.set_data(data);
+        store.set_sorter(default_sorter);
+
+        let selection =
+            Selection::new().on_select(ctx.link().callback(|_| Self::Message::SelectionChange));
+
+        Self { store, selection }
+    }
+
+    fn view(&self, _ctx: &Context<Self>) -> Html {
+        let columns = Self::columns(self.store.clone());
+
+        let table = DataTable::new(columns, 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);
+
+        Column::new()
+            .class(pwt::css::FlexFit)
+            .with_child(table)
+            .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)
+        {
+            let data = zones_to_vrf_view(
+                &ctx.props().controllers,
+                &ctx.props().zones,
+                &ctx.props().vnets,
+            );
+
+            self.store.set_data(data);
+            self.store.set_sorter(default_sorter);
+
+            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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v2 11/15] ui: sdn: add AddVnetWindow component
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (26 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 10/15] ui: add view for showing ip vrfs Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 12/15] ui: sdn: add AddZoneWindow component Stefan Hanreich
                   ` (6 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 UTC (permalink / raw)
  To: pdm-devel

Adds an edit window for creating a new VNet across multiple remotes.
Currently it

This windows shows a form
containing all fields required to create new VNet via the create_vnet
API endpoint.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 lib/pdm-client/src/lib.rs   |   2 +-
 ui/src/sdn/evpn/add_vnet.rs | 273 ++++++++++++++++++++++++++++++++++++
 ui/src/sdn/evpn/mod.rs      |   3 +
 3 files changed, 277 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 1e4cace..bb14a68 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::{
-        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..6ec0eb2
--- /dev/null
+++ b/ui/src/sdn/evpn/add_vnet.rs
@@ -0,0 +1,273 @@
+use std::{collections::HashSet, rc::Rc};
+
+use anyhow::{bail, 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,
+}
+
+pub enum ZoneTableMsg {
+    SelectionChange,
+}
+
+impl ManagedField for ZoneTableComponent {
+    type Properties = ZoneTable;
+    type Message = ZoneTableMsg;
+    type ValidateClosure = ();
+
+    fn validation_args(_props: &Self::Properties) -> Self::ValidateClosure {}
+
+    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() {
+            bail!("at least one remote needs to be selected");
+        }
+
+        let mut unique = HashSet::new();
+
+        if !selected_entries
+            .iter()
+            .all(|entry| unique.insert(entry.remote.as_str()))
+        {
+            bail!("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 link = ctx.link().clone();
+        let selection = Selection::new().multiselect(true).on_select(move |_| {
+            link.send_message(Self::Message::SelectionChange);
+        });
+
+        let store = Store::new();
+        store.set_data(
+            ctx.props()
+                .zones
+                .iter()
+                .map(|zone| ZoneTableEntry {
+                    remote: zone.remote.clone(),
+                    zone: zone.zone.zone.clone(),
+                    vni: zone.zone.vrf_vxlan.expect("evpn zone as a vrf vni"),
+                })
+                .collect(),
+        );
+
+        Self { store, selection }
+    }
+
+    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(COLUMNS.with(Rc::clone), self.store.clone())
+            .multiselect_mode(MultiSelectMode::Simple)
+            .border(true)
+            .class(css::FlexFit);
+
+        Container::new()
+            .with_child(GridPicker::new(table).selection(self.selection.clone()))
+            .into()
+    }
+}
+
+thread_local! {
+    static 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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v2 12/15] ui: sdn: add AddZoneWindow component
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (27 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 11/15] ui: sdn: add AddVnetWindow component Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 13/15] ui: sdn: add EvpnPanel Stefan Hanreich
                   ` (5 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 ui/src/sdn/evpn/add_zone.rs | 281 ++++++++++++++++++++++++++++++++++++
 ui/src/sdn/evpn/mod.rs      |   3 +
 2 files changed, 284 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..84533fe
--- /dev/null
+++ b/ui/src/sdn/evpn/add_zone.rs
@@ -0,0 +1,281 @@
+use std::{collections::HashSet, rc::Rc};
+
+use anyhow::{bail, 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,
+}
+
+enum ControllerTableMsg {
+    SelectionChange,
+}
+
+impl ManagedField for ControllerTableComponent {
+    type Properties = ControllerTable;
+    type Message = ControllerTableMsg;
+    type ValidateClosure = ();
+
+    fn validation_args(_props: &Self::Properties) -> Self::ValidateClosure {}
+
+    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() {
+            bail!("at least one remote needs to be selected");
+        }
+
+        let mut unique = HashSet::new();
+
+        if !selected_entries
+            .iter()
+            .all(|entry| unique.insert(entry.remote.as_str()))
+        {
+            bail!("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();
+        store.set_data(
+            ctx.props()
+                .controllers
+                .iter()
+                .map(|controller| ControllerTableEntry {
+                    remote: controller.remote.clone(),
+                    controller: controller.controller.controller.clone(),
+                    asn: controller.controller.asn.expect("EVPN controller has ASN"),
+                })
+                .collect(),
+        );
+
+        Self { store, selection }
+    }
+
+    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(COLUMNS.with(Rc::clone), self.store.clone())
+            .multiselect_mode(MultiSelectMode::Simple)
+            .border(true)
+            .class(css::FlexFit);
+
+        Container::new()
+            .with_child(GridPicker::new(table).selection(self.selection.clone()))
+            .into()
+    }
+}
+
+thread_local! {
+    static 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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v2 13/15] ui: sdn: add EvpnPanel
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (28 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 12/15] ui: sdn: add AddZoneWindow component Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 14/15] ui: sdn: add EvpnPanel to main menu Stefan Hanreich
                   ` (4 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 ui/src/sdn/evpn/evpn_panel.rs | 224 ++++++++++++++++++++++++++++++++++
 ui/src/sdn/evpn/mod.rs        |   3 +
 2 files changed, 227 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..930d8e4
--- /dev/null
+++ b/ui/src/sdn/evpn/evpn_panel.rs
@@ -0,0 +1,224 @@
+use futures::try_join;
+use std::rc::Rc;
+
+use anyhow::Error;
+use yew::virtual_dom::{VComp, VNode};
+use yew::{Callback, Html, Properties};
+
+use pdm_client::types::{ListController, ListControllersType, ListVnet, ListZone, ListZonesType};
+use proxmox_yew_comp::{LoadableComponent, LoadableComponentContext, LoadableComponentMaster};
+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>>,
+}
+
+impl LoadableComponent for EvpnPanelComponent {
+    type Properties = EvpnPanel;
+    type Message = EvpnPanelMsg;
+    type ViewState = EvpnPanelViewState;
+
+    fn create(_ctx: &LoadableComponentContext<Self>) -> Self {
+        Self {
+            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;
+
+                return true;
+            }
+            Self::Message::Reload => {
+                ctx.link().send_reload();
+            }
+        }
+
+        false
+    }
+
+    fn main_view(&self, ctx: &LoadableComponentContext<Self>) -> Html {
+        let add_menu = Menu::new()
+            .with_item(
+                MenuItem::new(tr!("Zone")).icon_class("fa fa-th").on_select(
+                    ctx.link()
+                        .change_view_callback(|_| Some(Self::ViewState::AddZone)),
+                ),
+            )
+            .with_item(
+                MenuItem::new(tr!("VNet"))
+                    .icon_class("fa fa-sdn-vnet")
+                    .on_select(
+                        ctx.link()
+                            .change_view_callback(|_| Some(Self::ViewState::AddVnet)),
+                    ),
+            );
+
+        let toolbar = 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(false).onclick(ctx.link().callback(|_| Self::Message::Reload)),
+            );
+
+        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"),
+                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"),
+                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(toolbar)
+            .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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v2 14/15] ui: sdn: add EvpnPanel to main menu
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (29 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 13/15] ui: sdn: add EvpnPanel Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 15/15] pve: sdn: add descriptions for sdn tasks Stefan Hanreich
                   ` (3 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 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 4f40d2c..68068e4 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,
@@ -286,6 +287,15 @@ impl Component for PdmMainMenu {
             remote_submenu,
         );
 
+        register_view(
+            &mut menu,
+            &mut content,
+            tr!("EVPN"),
+            "evpn",
+            Some("fa fa-key"),
+            |_| EvpnPanel::new().into(),
+        );
+
         let drawer = NavigationDrawer::new(menu)
             .aria_label("Datacenter Manager")
             .class("pwt-border-end")
-- 
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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v2 15/15] pve: sdn: add descriptions for sdn tasks
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (30 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 14/15] ui: sdn: add EvpnPanel to main menu Stefan Hanreich
@ 2025-08-22 13:49 ` Stefan Hanreich
  2025-08-26 12:22 ` [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Gabriel Goller
                   ` (2 subsequent siblings)
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-22 13:49 UTC (permalink / raw)
  To: pdm-devel

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

diff --git a/ui/src/tasks.rs b/ui/src/tasks.rs
index 6aa202a..1ce0c83 100644
--- a/ui/src/tasks.rs
+++ b/ui/src/tasks.rs
@@ -65,9 +65,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] 43+ messages in thread

* Re: [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (31 preceding siblings ...)
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 15/15] pve: sdn: add descriptions for sdn tasks Stefan Hanreich
@ 2025-08-26 12:22 ` Gabriel Goller
  2025-08-26 14:06   ` Stefan Hanreich
  2025-08-26 14:24 ` Dominik Csapak
  2025-08-27 11:35 ` [pdm-devel] superseded: " Stefan Hanreich
  34 siblings, 1 reply; 43+ messages in thread
From: Gabriel Goller @ 2025-08-26 12:22 UTC (permalink / raw)
  To: Stefan Hanreich; +Cc: pdm-devel

Don't have much experience with PDM, so I haven't looked at the code.

The patches don't apply cleanly anymore, but after a bit of manual
tinkering it works. As we found out together filtering vnets doesn't
work so we need to filter in the frontend for vnets which have a evpn
zone. The following patch fixes it:

diff --git a/ui/src/sdn/evpn/remote_tree.rs b/ui/src/sdn/evpn/remote_tree.rs
index e4b0fe46a121..4077693d29df 100644
--- a/ui/src/sdn/evpn/remote_tree.rs
+++ b/ui/src/sdn/evpn/remote_tree.rs
@@ -207,13 +207,12 @@ fn zones_to_vrf_view(
      for vnet in vnets {
          let vnet_data = &vnet.vnet;

-        let zone = zones
-            .iter()
-            .find(|zone| {
-                zone.remote == vnet.remote
-                    && vnet_data.zone.as_ref().expect("vnet has zone") == &zone.zone.zone
-            })
-            .expect("vnet has zone");
+        let Some(zone) = zones.iter().find(|zone| {
+            zone.remote == vnet.remote
+                && vnet_data.zone.as_ref().expect("vnet has zone") == &zone.zone.zone
+        }) else {
+            continue;
+        };

          let controller = controllers
              .iter()
diff --git a/ui/src/sdn/evpn/vrf_tree.rs b/ui/src/sdn/evpn/vrf_tree.rs
index 8b01b00eba26..2980d2379b0a 100644
--- a/ui/src/sdn/evpn/vrf_tree.rs
+++ b/ui/src/sdn/evpn/vrf_tree.rs
@@ -155,13 +155,12 @@ fn zones_to_vrf_view(
      for vnet in vnets {
          let vnet_data = &vnet.vnet;

-        let zone = zones
-            .iter()
-            .find(|zone| {
-                zone.remote == vnet.remote
-                    && vnet_data.zone.as_ref().expect("vnet has zone") == &zone.zone.zone
-            })
-            .expect("zone of vnet exists");
+        let Some(zone) = zones.iter().find(|zone| {
+            zone.remote == vnet.remote
+                && vnet_data.zone.as_ref().expect("vnet has zone") == &zone.zone.zone
+        }) else {
+            continue;
+        };

          let controller = controllers
              .iter()


A few small UI nits:
  * In the "Remotes" view, widen the "Name" column a bit, it's too narrow
  * In the "IP-VRFs" view, when fully expanding all the VNets, the "VNet"
    level text is indented more than the last child. So the "VNet" text
    is more to the left than the actual VNet name below. (I think this is
    the VNet icon missing.)
  * Clicking the refresh button should IMO not collapse the tree.

  * When clicking on "Add" the VNet icon is missing (The zone icon is
    there).
  * What about deleting zones and vnets? Would that be complex? If a
    remote fails to delete a vnet/zone, we could roll-back all the other
    ones using the lock thingy-right?
  * I think there is a min-character limit missing on the vnet name, I
    get:
      2025-08-26T14:16:38+02:00: failed to execute transaction on remote andiknowbangers: api error (status = 400: Parameter verification failed.
      vnet: invalid format - vnet ID 't' contains illegal characters
    when creating a vnet named "t".
  * IDK about the EVPN icon being a key :) Maybe we should already create a SDN
    "folder" and then an EVPN entry (like in PVE) so that we are ready
    adding more stuff afterwards.
  * Do we really want to auto-apply everything immediately? Should we
    maybe introduce a SDN "apply" thingy in PDM as well?


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


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

* Re: [pdm-devel] [PATCH proxmox-yew-comp v2 1/1] sdn: add descriptions for sdn tasks
  2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-yew-comp v2 1/1] sdn: add descriptions for sdn tasks Stefan Hanreich
@ 2025-08-26 13:22   ` Dominik Csapak
  2025-08-26 14:06     ` Stefan Hanreich
  0 siblings, 1 reply; 43+ messages in thread
From: Dominik Csapak @ 2025-08-26 13:22 UTC (permalink / raw)
  To: Proxmox Datacenter Manager development discussion, Stefan Hanreich

this probably fits better in pdm directly? we have the most pve task 
descriptions in ui/src/tasks.rs

proxmox-yew-comp tries to be product agnostic and i guess this is only
pve specific? (e.g. no evpn zones on pbs ;) )

On 8/22/25 3:50 PM, Stefan Hanreich wrote:
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
>   src/utils.rs | 3 +++
>   1 file changed, 3 insertions(+)
> 
> diff --git a/src/utils.rs b/src/utils.rs
> index 1a4ad40..3d5def1 100644
> --- a/src/utils.rs
> +++ b/src/utils.rs
> @@ -225,6 +225,9 @@ pub fn init_task_descr_table_base() {
>       register_task_description("srvstop", (tr!("Service"), tr!("Stop")));
>       register_task_description("srvrestart", (tr!("Service"), tr!("Restart")));
>       register_task_description("srvreload", (tr!("Service"), tr!("Reload")));
> +
> +    register_task_description("create_zone", tr!("Create EVPN VRF (Zone)"));
> +    register_task_description("create_vnet", tr!("Create EVPN VNet"));
>   }
>   
>   /// Uses information from the given [`UPID`] to render the task description with [`format_task_description`]



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


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

* Re: [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration
  2025-08-26 12:22 ` [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Gabriel Goller
@ 2025-08-26 14:06   ` Stefan Hanreich
  2025-08-26 14:12     ` Dominik Csapak
  0 siblings, 1 reply; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-26 14:06 UTC (permalink / raw)
  To: pdm-devel

On 8/26/25 2:21 PM, Gabriel Goller wrote:
> Don't have much experience with PDM, so I haven't looked at the code.
> 
> The patches don't apply cleanly anymore, but after a bit of manual
> tinkering it works. As we found out together filtering vnets doesn't
> work so we need to filter in the frontend for vnets which have a evpn
> zone. The following patch fixes it:

will rebase in a v3

> diff --git a/ui/src/sdn/evpn/remote_tree.rs b/ui/src/sdn/evpn/
> remote_tree.rs
> index e4b0fe46a121..4077693d29df 100644
> --- a/ui/src/sdn/evpn/remote_tree.rs
> +++ b/ui/src/sdn/evpn/remote_tree.rs
> @@ -207,13 +207,12 @@ fn zones_to_vrf_view(
>      for vnet in vnets {
>          let vnet_data = &vnet.vnet;
> 
> -        let zone = zones
> -            .iter()
> -            .find(|zone| {
> -                zone.remote == vnet.remote
> -                    && vnet_data.zone.as_ref().expect("vnet has zone")
> == &zone.zone.zone
> -            })
> -            .expect("vnet has zone");
> +        let Some(zone) = zones.iter().find(|zone| {
> +            zone.remote == vnet.remote
> +                && vnet_data.zone.as_ref().expect("vnet has zone") ==
> &zone.zone.zone
> +        }) else {
> +            continue;
> +        };
> 
>          let controller = controllers
>              .iter()
> diff --git a/ui/src/sdn/evpn/vrf_tree.rs b/ui/src/sdn/evpn/vrf_tree.rs
> index 8b01b00eba26..2980d2379b0a 100644
> --- a/ui/src/sdn/evpn/vrf_tree.rs
> +++ b/ui/src/sdn/evpn/vrf_tree.rs
> @@ -155,13 +155,12 @@ fn zones_to_vrf_view(
>      for vnet in vnets {
>          let vnet_data = &vnet.vnet;
> 
> -        let zone = zones
> -            .iter()
> -            .find(|zone| {
> -                zone.remote == vnet.remote
> -                    && vnet_data.zone.as_ref().expect("vnet has zone")
> == &zone.zone.zone
> -            })
> -            .expect("zone of vnet exists");
> +        let Some(zone) = zones.iter().find(|zone| {
> +            zone.remote == vnet.remote
> +                && vnet_data.zone.as_ref().expect("vnet has zone") ==
> &zone.zone.zone
> +        }) else {
> +            continue;
> +        };
> 
>          let controller = controllers
>              .iter()
> 
> 
> A few small UI nits:
>  * In the "Remotes" view, widen the "Name" column a bit, it's too narrow
>  * In the "IP-VRFs" view, when fully expanding all the VNets, the "VNet"
>    level text is indented more than the last child. So the "VNet" text
>    is more to the left than the actual VNet name below. (I think this is
>    the VNet icon missing.)
>  * Clicking the refresh button should IMO not collapse the tree.

Not sure how I can prevent that, since I rebuild the tree after every
request so it will 'reset' to the collapsed state. Maybe @Dominik has an
idea?

>  * When clicking on "Add" the VNet icon is missing (The zone icon is
>    there).

Weird, the icon should be included in the patch series and showed
correctly on my machine. I'll check it out!

>  * What about deleting zones and vnets? Would that be complex? If a
>    remote fails to delete a vnet/zone, we could roll-back all the other
>    ones using the lock thingy-right?

shouldn't be too hard to add in a future patch series, but I had to make
a scope cut somewhere for now since I was already running quite late
with the series.

>  * I think there is a min-character limit missing on the vnet name, I
>    get:
>      2025-08-26T14:16:38+02:00: failed to execute transaction on remote
> andiknowbangers: api error (status = 400: Parameter verification failed.
>      vnet: invalid format - vnet ID 't' contains illegal characters
>    when creating a vnet named "t".
>  * IDK about the EVPN icon being a key :) Maybe we should already create
> a SDN
>    "folder" and then an EVPN entry (like in PVE) so that we are ready>
   adding more stuff afterwards.
>  * Do we really want to auto-apply everything immediately? Should we
>    maybe introduce a SDN "apply" thingy in PDM as well?

In the future we could certainly implement that, but currently we are
only using the running configuration as source for the PDM tree.

If we want to render things correctly on PDM side after adding Zones /
VNets, we'd have to read the pending configuration and then special case
new / changed / deleted entities and respect that state while merging -
which is more complex than this current approach so I've left it out for
now. Certainly something for the future though.


Thanks for the review, I'll incorporate your suggestions in a v3!



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

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

* Re: [pdm-devel] [PATCH proxmox-yew-comp v2 1/1] sdn: add descriptions for sdn tasks
  2025-08-26 13:22   ` Dominik Csapak
@ 2025-08-26 14:06     ` Stefan Hanreich
  0 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-26 14:06 UTC (permalink / raw)
  To: Dominik Csapak, Proxmox Datacenter Manager development discussion

On 8/26/25 3:22 PM, Dominik Csapak wrote:
> this probably fits better in pdm directly? we have the most pve task
> descriptions in ui/src/tasks.rs
> 
> proxmox-yew-comp tries to be product agnostic and i guess this is only
> pve specific? (e.g. no evpn zones on pbs ;) )

Ah, I just didn't notice that - will add it to PDM directly in the v3!


[snip]


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


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

* Re: [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration
  2025-08-26 14:06   ` Stefan Hanreich
@ 2025-08-26 14:12     ` Dominik Csapak
  2025-08-26 14:13       ` Stefan Hanreich
  0 siblings, 1 reply; 43+ messages in thread
From: Dominik Csapak @ 2025-08-26 14:12 UTC (permalink / raw)
  To: Proxmox Datacenter Manager development discussion, Stefan Hanreich



On 8/26/25 4:07 PM, Stefan Hanreich wrote:
> On 8/26/25 2:21 PM, Gabriel Goller wrote:
>> Don't have much experience with PDM, so I haven't looked at the code.
>>
>> The patches don't apply cleanly anymore, but after a bit of manual
>> tinkering it works. As we found out together filtering vnets doesn't
>> work so we need to filter in the frontend for vnets which have a evpn
>> zone. The following patch fixes it:
> 
> will rebase in a v3
> 
>> diff --git a/ui/src/sdn/evpn/remote_tree.rs b/ui/src/sdn/evpn/
>> remote_tree.rs
>> index e4b0fe46a121..4077693d29df 100644
>> --- a/ui/src/sdn/evpn/remote_tree.rs
>> +++ b/ui/src/sdn/evpn/remote_tree.rs
>> @@ -207,13 +207,12 @@ fn zones_to_vrf_view(
>>       for vnet in vnets {
>>           let vnet_data = &vnet.vnet;
>>
>> -        let zone = zones
>> -            .iter()
>> -            .find(|zone| {
>> -                zone.remote == vnet.remote
>> -                    && vnet_data.zone.as_ref().expect("vnet has zone")
>> == &zone.zone.zone
>> -            })
>> -            .expect("vnet has zone");
>> +        let Some(zone) = zones.iter().find(|zone| {
>> +            zone.remote == vnet.remote
>> +                && vnet_data.zone.as_ref().expect("vnet has zone") ==
>> &zone.zone.zone
>> +        }) else {
>> +            continue;
>> +        };
>>
>>           let controller = controllers
>>               .iter()
>> diff --git a/ui/src/sdn/evpn/vrf_tree.rs b/ui/src/sdn/evpn/vrf_tree.rs
>> index 8b01b00eba26..2980d2379b0a 100644
>> --- a/ui/src/sdn/evpn/vrf_tree.rs
>> +++ b/ui/src/sdn/evpn/vrf_tree.rs
>> @@ -155,13 +155,12 @@ fn zones_to_vrf_view(
>>       for vnet in vnets {
>>           let vnet_data = &vnet.vnet;
>>
>> -        let zone = zones
>> -            .iter()
>> -            .find(|zone| {
>> -                zone.remote == vnet.remote
>> -                    && vnet_data.zone.as_ref().expect("vnet has zone")
>> == &zone.zone.zone
>> -            })
>> -            .expect("zone of vnet exists");
>> +        let Some(zone) = zones.iter().find(|zone| {
>> +            zone.remote == vnet.remote
>> +                && vnet_data.zone.as_ref().expect("vnet has zone") ==
>> &zone.zone.zone
>> +        }) else {
>> +            continue;
>> +        };
>>
>>           let controller = controllers
>>               .iter()
>>
>>
>> A few small UI nits:
>>   * In the "Remotes" view, widen the "Name" column a bit, it's too narrow
>>   * In the "IP-VRFs" view, when fully expanding all the VNets, the "VNet"
>>     level text is indented more than the last child. So the "VNet" text
>>     is more to the left than the actual VNet name below. (I think this is
>>     the VNet icon missing.)
>>   * Clicking the refresh button should IMO not collapse the tree.
> 
> Not sure how I can prevent that, since I rebuild the tree after every
> request so it will 'reset' to the collapsed state. Maybe @Dominik has an
> idea?
> 

for the root node of a tree, there are
`extract_expanded_state()` and `apply_expanded_state()` so
the idea here is you extract the state, refresh, and then apply the old
state again. assuming the keys did not change, this should keep the
expansion state


>>   * When clicking on "Add" the VNet icon is missing (The zone icon is
>>     there).
> 
> Weird, the icon should be included in the patch series and showed
> correctly on my machine. I'll check it out!
> 
>>   * What about deleting zones and vnets? Would that be complex? If a
>>     remote fails to delete a vnet/zone, we could roll-back all the other
>>     ones using the lock thingy-right?
> 
> shouldn't be too hard to add in a future patch series, but I had to make
> a scope cut somewhere for now since I was already running quite late
> with the series.
> 
>>   * I think there is a min-character limit missing on the vnet name, I
>>     get:
>>       2025-08-26T14:16:38+02:00: failed to execute transaction on remote
>> andiknowbangers: api error (status = 400: Parameter verification failed.
>>       vnet: invalid format - vnet ID 't' contains illegal characters
>>     when creating a vnet named "t".
>>   * IDK about the EVPN icon being a key :) Maybe we should already create
>> a SDN
>>     "folder" and then an EVPN entry (like in PVE) so that we are ready>
>     adding more stuff afterwards.
>>   * Do we really want to auto-apply everything immediately? Should we
>>     maybe introduce a SDN "apply" thingy in PDM as well?
> 
> In the future we could certainly implement that, but currently we are
> only using the running configuration as source for the PDM tree.
> 
> If we want to render things correctly on PDM side after adding Zones /
> VNets, we'd have to read the pending configuration and then special case
> new / changed / deleted entities and respect that state while merging -
> which is more complex than this current approach so I've left it out for
> now. Certainly something for the future though.
> 
> 
> Thanks for the review, I'll incorporate your suggestions in a v3!
> 
> 
> 
> _______________________________________________
> pdm-devel mailing list
> pdm-devel@lists.proxmox.com
> https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel



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

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

* Re: [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration
  2025-08-26 14:12     ` Dominik Csapak
@ 2025-08-26 14:13       ` Stefan Hanreich
  0 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-26 14:13 UTC (permalink / raw)
  To: Dominik Csapak, Proxmox Datacenter Manager development discussion

On 8/26/25 4:12 PM, Dominik Csapak wrote:
[snip]

>>>   * Clicking the refresh button should IMO not collapse the tree.
>>
>> Not sure how I can prevent that, since I rebuild the tree after every
>> request so it will 'reset' to the collapsed state. Maybe @Dominik has an
>> idea?
>>
> 
> for the root node of a tree, there are
> `extract_expanded_state()` and `apply_expanded_state()` so
> the idea here is you extract the state, refresh, and then apply the old
> state again. assuming the keys did not change, this should keep the
> expansion state

Definitely missed that - thanks for the pointer! I'll fix that in the v3
then!

[snip]


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

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

* Re: [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (32 preceding siblings ...)
  2025-08-26 12:22 ` [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Gabriel Goller
@ 2025-08-26 14:24 ` Dominik Csapak
  2025-08-26 14:25   ` Stefan Hanreich
  2025-08-27 11:35 ` [pdm-devel] superseded: " Stefan Hanreich
  34 siblings, 1 reply; 43+ messages in thread
From: Dominik Csapak @ 2025-08-26 14:24 UTC (permalink / raw)
  To: Proxmox Datacenter Manager development discussion, Stefan Hanreich

high level comment, not sure what i'm doing wrong
but i get a

Application panicked!
Reason: panicked at src/sdn/evpn/vrf_tree.rs:164:14: zone of vnet exists

when clicking on the evpn gui ;)

maybe i misconfigured my nodes, but IMHO the gui shouldn't crash because 
of that ;)


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


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

* Re: [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration
  2025-08-26 14:24 ` Dominik Csapak
@ 2025-08-26 14:25   ` Stefan Hanreich
  0 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-26 14:25 UTC (permalink / raw)
  To: Dominik Csapak, Proxmox Datacenter Manager development discussion

On 8/26/25 4:24 PM, Dominik Csapak wrote:
> high level comment, not sure what i'm doing wrong
> but i get a
> 
> Application panicked!
> Reason: panicked at src/sdn/evpn/vrf_tree.rs:164:14: zone of vnet exists
> 
> when clicking on the evpn gui ;)
> 
> maybe i misconfigured my nodes, but IMHO the gui shouldn't crash because
> of that ;)

That should be fixed by the patch from Gabriel.


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


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

* [pdm-devel] superseded: [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration
  2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (33 preceding siblings ...)
  2025-08-26 14:24 ` Dominik Csapak
@ 2025-08-27 11:35 ` Stefan Hanreich
  34 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-27 11:35 UTC (permalink / raw)
  To: pdm-devel

https://lore.proxmox.com/pdm-devel/20250827113427.199253-1-s.hanreich@proxmox.com/T/#t

On 8/22/25 3:49 PM, 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
> 
> 
> ## 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)
> 
> 
> ## Changelog
> 
> Changes since the 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-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 proxmox-yew-comp
> 
> 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             | 203 ++++++++++++++++--
>  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      |  15 +-
>  13 files changed, 457 insertions(+), 48 deletions(-)
> 
> 
> proxmox-api-types:
> 
> Stefan Hanreich (7):
>   add QemuMigratePreconditionsNotAllowedNodesBlockingHaResources struct
>   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      | 40 ++++++++++++++++++++++++++++++++++
>  pve-api-types/src/lib.rs       |  1 +
>  pve-api-types/src/sdn.rs       | 33 ++++++++++++++++++++++++++++
>  pve-api-types/src/types/mod.rs |  4 ++++
>  4 files changed, 78 insertions(+)
>  create mode 100644 pve-api-types/src/sdn.rs
> 
> 
> proxmox-yew-comp:
> 
> Stefan Hanreich (1):
>   sdn: add descriptions for sdn tasks
> 
>  src/utils.rs | 3 +++
>  1 file changed, 3 insertions(+)
> 
> 
> proxmox-datacenter-manager:
> 
> Stefan Hanreich (15):
>   server: add locked sdn client helpers
>   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 remote tree component
>   ui: add view for showing ip vrfs
>   ui: sdn: add AddVnetWindow component
>   ui: sdn: add AddZoneWindow component
>   ui: sdn: add EvpnPanel
>   ui: sdn: add EvpnPanel to main menu
>   pve: sdn: add descriptions for sdn tasks
> 
>  lib/pdm-api-types/Cargo.toml      |   2 +
>  lib/pdm-api-types/src/lib.rs      |   2 +
>  lib/pdm-api-types/src/sdn.rs      | 168 +++++++++++++
>  lib/pdm-client/src/lib.rs         |  61 +++++
>  server/src/api/mod.rs             |   2 +
>  server/src/api/sdn/controllers.rs |  78 ++++++
>  server/src/api/sdn/mod.rs         |  17 ++
>  server/src/api/sdn/vnets.rs       | 149 +++++++++++
>  server/src/api/sdn/zones.rs       | 174 +++++++++++++
>  server/src/lib.rs                 |   1 +
>  server/src/sdn_client.rs          | 387 ++++++++++++++++++++++++++++
>  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       | 273 ++++++++++++++++++++
>  ui/src/sdn/evpn/add_zone.rs       | 281 +++++++++++++++++++++
>  ui/src/sdn/evpn/evpn_panel.rs     | 224 +++++++++++++++++
>  ui/src/sdn/evpn/mod.rs            |  41 +++
>  ui/src/sdn/evpn/remote_tree.rs    | 403 ++++++++++++++++++++++++++++++
>  ui/src/sdn/evpn/vrf_tree.rs       | 345 +++++++++++++++++++++++++
>  ui/src/sdn/mod.rs                 |   1 +
>  ui/src/tasks.rs                   |   2 +
>  23 files changed, 2642 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, 3214 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] 43+ messages in thread

* [pdm-devel] [PATCH proxmox-datacenter-manager v2 01/15] server: add locked sdn client helpers
  2025-08-29 14:52 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v2 00/30] " Stefan Hanreich
@ 2025-08-29 14:52 ` Stefan Hanreich
  0 siblings, 0 replies; 43+ messages in thread
From: Stefan Hanreich @ 2025-08-29 14:52 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 | 427 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 428 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..c9e13ad
--- /dev/null
+++ b/server/src/sdn_client.rs
@@ -0,0 +1,427 @@
+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 {
+    secret: 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(|secret| Self { secret, 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.secret.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.secret.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.secret.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.secret),
+        };
+
+        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.secret),
+            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, _) in clients {
+                        proxmox_log::info!("releasing lock for remote {}", 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)
+                        }),
+                    };
+                }
+            };
+        }
+
+        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 {
+                    Ok(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] 43+ messages in thread

end of thread, other threads:[~2025-08-29 15:00 UTC | newest]

Thread overview: 43+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-08-22 13:49 [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox v2 1/2] schema: use i64 for minimum / maximum / default integer values Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox v2 2/2] pbs-api-types: fix values for integer schemas Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-backup v2 1/1] api: change integer schema parameters to i64 Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH pve-network v2 1/6] sdn: api: return null for rollback / lock endpoints Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH pve-network v2 2/6] controllers: fix maximum value for ASN Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH pve-network v2 3/6] api: add state standard option Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH pve-network v2 4/6] api: controllers: update schema of endpoints Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH pve-network v2 5/6] api: vnets: " Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH pve-network v2 6/6] api: zones: " Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-api-types v2 1/7] add QemuMigratePreconditionsNotAllowedNodesBlockingHaResources struct Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-api-types v2 2/7] sdn: add list/create zone endpoints Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-api-types v2 3/7] sdn: add list/create vnet endpoints Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-api-types v2 4/7] sdn: add list/create controller endpoints Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-api-types v2 5/7] sdn: add sdn configuration locking endpoints Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-api-types v2 6/7] tasks: add helper for querying successfully finished tasks Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-api-types v2 7/7] sdn: add helpers for pending values Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-yew-comp v2 1/1] sdn: add descriptions for sdn tasks Stefan Hanreich
2025-08-26 13:22   ` Dominik Csapak
2025-08-26 14:06     ` Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 01/15] server: add locked sdn client helpers Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 02/15] api: sdn: add list_zones endpoint Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 03/15] api: sdn: add create_zone endpoint Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 04/15] api: sdn: add list_vnets endpoint Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 05/15] api: sdn: add create_vnet endpoint Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 06/15] api: sdn: add list_controllers endpoint Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 07/15] ui: sdn: add EvpnRouteTarget type Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 08/15] ui: sdn: add vnet icon Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 09/15] ui: sdn: add remote tree component Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 10/15] ui: add view for showing ip vrfs Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 11/15] ui: sdn: add AddVnetWindow component Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 12/15] ui: sdn: add AddZoneWindow component Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 13/15] ui: sdn: add EvpnPanel Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 14/15] ui: sdn: add EvpnPanel to main menu Stefan Hanreich
2025-08-22 13:49 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 15/15] pve: sdn: add descriptions for sdn tasks Stefan Hanreich
2025-08-26 12:22 ` [pdm-devel] [RFC network/proxmox{, -backup, -api-types, -yew-comp, -datacenter-manager} v2 00/32] Add initial SDN / EVPN integration Gabriel Goller
2025-08-26 14:06   ` Stefan Hanreich
2025-08-26 14:12     ` Dominik Csapak
2025-08-26 14:13       ` Stefan Hanreich
2025-08-26 14:24 ` Dominik Csapak
2025-08-26 14:25   ` Stefan Hanreich
2025-08-27 11:35 ` [pdm-devel] superseded: " Stefan Hanreich
2025-08-29 14:52 [pdm-devel] [PATCH network/proxmox{, -backup, -api-types, -datacenter-manager} v2 00/30] " Stefan Hanreich
2025-08-29 14:52 ` [pdm-devel] [PATCH proxmox-datacenter-manager v2 01/15] server: add locked sdn client helpers Stefan Hanreich

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal