public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration
@ 2025-02-28 15:17 Stefan Hanreich
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 01/12] sdn: add list/create zone endpoints Stefan Hanreich
                   ` (25 more replies)
  0 siblings, 26 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:17 UTC (permalink / raw)
  To: pdm-devel

## Introduction

This patch series introduces the first SDN features to the Proxmox Datacenter
Manager. I sent it as an RFC, since I wanted to get some early feedback and
nothing presented here is set in stone.

This patch series is mainly intended for laying the groundwork on how to handle
SDN configuration changes across multiple remotes. To demonstrate the concept it
implements two, relatively simple, features using the newly introduced lock
functionality:

* Creating a new VRF (= zone) across multiple remotes
* Creating a new VNet across multiple remotes

Additionally there is an aggregated overview of all EVPN zones / controllers of
all PVE remotes.

Once everything has been properly fleshed out, the groundwork laid here will be
used to build the full SDN/EVPN integration into PDM with more complex
configuration changes.


## Prerequisites

This patch series relies on the following preparatory patch series, that I've
sent separately:

https://lore.proxmox.com/pve-devel/20250228130549.100357-1-s.hanreich@proxmox.com/T/
https://lore.proxmox.com/pve-devel/20250228140136.124286-1-s.hanreich@proxmox.com/T/
https://lore.proxmox.com/pbs-devel/20250129104250.934927-1-s.hanreich@proxmox.com/T/

I've sent others, but as far as I can tell they have already been merged. Please
tell me if I missed something and you cannot build this patch series! Not all of
them have been bumped yet, so make sure to apply the patches on top of the
current master.


## How it works

I introduced a new locking mechanism to the SDN API, that locks the
configuration and enables subsequent modifications only when the lock secret is
provided. Locking the configuration only succeeds, if there are no pending
changes, but there's a knob for tuning that behavior (that we could expose in
the PDM UI as well). See my other patch series for more detailed information.

The two PDM API calls introduced in this patch series do the following sequence
of actions when trying to change the SDN configuration:

* lock the sdn configuration of all involved remotes
  * if locking at least one remote fails, release the lock on all remotes and
    return unsuccessfully without making any changes
* invoke the API endpoints to make the changes to the SDN configuration
  * if a call fails, no further changes will be made to the remote and the
    configuration will stay locked - no rollbacks
  * no further changes will be made to the remote after one call fails
* apply the configuration on all remotes where the changes were successful
  * any errors during applying the configuration on a remote will be logged
  * the configuration will stay locked if applying the configuration fails
* reload the network configuration on all nodes where applying was succesful
  * if reloading was unsuccessful, the configuration will be unlocked, but the
    user will get an error message (This is also how applying the configuration
    on a single cluster currently works).
  * the task will wait for all remotes to finish reloading and only return
    successfully if changing the configuration on all remotes was successful

We are doing no automatic rollback of configuration changes. If we want to
introduce automatic rollback from PDM, this is how we could do it with how the
new SDN lock functionality works: We lock the SDN configuration from PDM if
there are no pending changes. We then proceed to make our changes to the config.
If we run into any error, we can be sure that all pending changes were made by
PDM. This fact enables us to safely rollback the configuration changes we made
and unlock the SDN configuration. This could be opt-in as a global PDM setting
(or when starting the task).

In the case of network issues, we cannot really proceed with making
configuration changes or rolling back the configuration. The configuration
changes will stay on PVE side, and users can choose to unlock & rollback.


## Open issues

Currently, we need information about 3 types of entities in the UI (zones,
vnets, controllers). The PDM API calls make an API call for each of those types
to the remotes and aggregates them before returning them. This makes the calls
really expensive, since we need to make 3 API calls to every remote configured
in PDM.

In this series, the yew component makes this call to PDM once and stores all the
results, passing the entities to every child component. This way we do not need
to repeat those calls when opening an EditWindow, for instance. In the future I
want to add a proper caching mechanism on the server side. Since this patch
series was already quite extensive I've decided to cut it out of scope for now.
I will think a bit on the best way to implement caching for this and implement
it in a follow-up patch series or a v2.

The matching of zones to a VRF is based solely on their VRF VXLAN VNI, in the
future we should also consider the rt-import configuration key for matching VRFs
to each other.


## Dependencies
* proxmox-api-types depends on pve-network (from the separate patch series)
* proxmox-datacenter-manager depends on proxmox-api-types / proxmox-yew-comp

proxmox-api-types:

Stefan Hanreich (12):
  sdn: add list/create zone endpoints
  sdn: generate zones endpoints
  sdn: add list/create vnet endpoints
  sdn: generate vnet endpoints
  sdn: add list/create controller endpoints
  sdn: generate controller endpoints
  sdn: add acquire/release lock endpoints
  sdn: generate lock endpoints
  sdn: add apply configuration endpoint
  sdn: generate apply configuration endpoint
  tasks: add helper for querying successfully finished tasks
  sdn: add helpers for pending values

 pve-api-types/generate.pl            |   36 +
 pve-api-types/src/generated/code.rs  |  150 +-
 pve-api-types/src/generated/types.rs | 5259 +++++++++++++++++---------
 pve-api-types/src/lib.rs             |    1 +
 pve-api-types/src/sdn.rs             |   33 +
 pve-api-types/src/types/mod.rs       |    4 +
 6 files changed, 3609 insertions(+), 1874 deletions(-)
 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 (13):
  server: add locked sdn client and helper methods
  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: add VrfTree component
  ui: sdn: add RouterTable component
  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         |  64 +++++++
 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       | 151 ++++++++++++++++
 server/src/api/sdn/zones.rs       | 177 ++++++++++++++++++
 server/src/lib.rs                 |   1 +
 server/src/sdn_client.rs          | 234 ++++++++++++++++++++++++
 ui/src/lib.rs                     |   2 +
 ui/src/main_menu.rs               |  10 +
 ui/src/sdn/evpn/add_vnet.rs       | 216 ++++++++++++++++++++++
 ui/src/sdn/evpn/add_zone.rs       | 229 +++++++++++++++++++++++
 ui/src/sdn/evpn/evpn_panel.rs     | 249 +++++++++++++++++++++++++
 ui/src/sdn/evpn/mod.rs            |  14 ++
 ui/src/sdn/evpn/router_table.rs   | 125 +++++++++++++
 ui/src/sdn/evpn/vrf_tree.rs       | 291 ++++++++++++++++++++++++++++++
 ui/src/sdn/mod.rs                 |   1 +
 ui/src/tasks.rs                   |   2 +
 21 files changed, 2035 insertions(+)
 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/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/router_table.rs
 create mode 100644 ui/src/sdn/evpn/vrf_tree.rs
 create mode 100644 ui/src/sdn/mod.rs


Summary over all repositories:
  28 files changed, 5647 insertions(+), 1874 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] 27+ messages in thread

* [pdm-devel] [PATCH proxmox-api-types 01/12] sdn: add list/create zone endpoints
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
@ 2025-02-28 15:17 ` Stefan Hanreich
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 02/12] sdn: generate zones endpoints Stefan Hanreich
                   ` (24 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:17 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 8be737b..06baf31 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',
@@ -300,6 +306,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');
+
 # NOW DUMP THE CODE:
 #
 # We generate one file for API types, and one for API method calls.
-- 
2.39.5


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


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

* [pdm-devel] [PATCH proxmox-api-types 02/12] sdn: generate zones endpoints
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 01/12] sdn: add list/create zone endpoints Stefan Hanreich
@ 2025-02-28 15:17 ` Stefan Hanreich
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 03/12] sdn: add list/create vnet endpoints Stefan Hanreich
                   ` (23 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:17 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 pve-api-types/src/generated/code.rs  |  39 +-
 pve-api-types/src/generated/types.rs | 789 +++++++++++++++++++++++++++
 2 files changed, 827 insertions(+), 1 deletion(-)

diff --git a/pve-api-types/src/generated/code.rs b/pve-api-types/src/generated/code.rs
index 401d3bc..c1f0e74 100644
--- a/pve-api-types/src/generated/code.rs
+++ b/pve-api-types/src/generated/code.rs
@@ -116,6 +116,8 @@
 /// - /cluster/sdn/ipams
 /// - /cluster/sdn/ipams/{ipam}
 /// - /cluster/sdn/ipams/{ipam}/status
+/// - /cluster/sdn/lock
+/// - /cluster/sdn/rollback
 /// - /cluster/sdn/vnets
 /// - /cluster/sdn/vnets/{vnet}
 /// - /cluster/sdn/vnets/{vnet}/firewall
@@ -125,7 +127,6 @@
 /// - /cluster/sdn/vnets/{vnet}/ips
 /// - /cluster/sdn/vnets/{vnet}/subnets
 /// - /cluster/sdn/vnets/{vnet}/subnets/{subnet}
-/// - /cluster/sdn/zones
 /// - /cluster/sdn/zones/{zone}
 /// - /cluster/tasks
 /// - /nodes/{node}
@@ -410,6 +411,11 @@ pub trait PveClient {
         Err(Error::Other("create_token not implemented"))
     }
 
+    /// Create a new sdn zone object.
+    async fn create_zone(&self, params: CreateZone) -> Result<(), Error> {
+        Err(Error::Other("create_zone not implemented"))
+    }
+
     /// Read subscription info.
     async fn get_subscription(&self, node: &str) -> Result<NodeSubscriptionInfo, Error> {
         Err(Error::Other("get_subscription not implemented"))
@@ -483,6 +489,16 @@ pub trait PveClient {
         Err(Error::Other("list_storages not implemented"))
     }
 
+    /// SDN zones index.
+    async fn list_zones(
+        &self,
+        pending: Option<bool>,
+        running: Option<bool>,
+        ty: Option<ListZonesType>,
+    ) -> Result<Vec<SdnZone>, Error> {
+        Err(Error::Other("list_zones not implemented"))
+    }
+
     /// Get container configuration.
     async fn lxc_get_config(
         &self,
@@ -710,6 +726,12 @@ where
         Ok(self.0.post(url, &params).await?.expect_json()?.data)
     }
 
+    /// Create a new sdn zone object.
+    async fn create_zone(&self, params: CreateZone) -> Result<(), Error> {
+        let url = "/api2/extjs/cluster/sdn/zones";
+        self.0.post(url, &params).await?.nodata()
+    }
+
     /// Read subscription info.
     async fn get_subscription(&self, node: &str) -> Result<NodeSubscriptionInfo, Error> {
         let url = &format!("/api2/extjs/nodes/{node}/subscription");
@@ -830,6 +852,21 @@ where
         Ok(self.0.get(url).await?.expect_json()?.data)
     }
 
+    /// SDN zones index.
+    async fn list_zones(
+        &self,
+        pending: Option<bool>,
+        running: Option<bool>,
+        ty: Option<ListZonesType>,
+    ) -> Result<Vec<SdnZone>, Error> {
+        let (mut query, mut sep) = (String::new(), '?');
+        add_query_bool(&mut query, &mut sep, "pending", pending);
+        add_query_bool(&mut query, &mut sep, "running", running);
+        add_query_arg(&mut query, &mut sep, "type", &ty);
+        let url = &format!("/api2/extjs/cluster/sdn/zones{query}");
+        Ok(self.0.get(url).await?.expect_json()?.data)
+    }
+
     /// Get container configuration.
     async fn lxc_get_config(
         &self,
diff --git a/pve-api-types/src/generated/types.rs b/pve-api-types/src/generated/types.rs
index 5a656ba..408d8ca 100644
--- a/pve-api-types/src/generated/types.rs
+++ b/pve-api-types/src/generated/types.rs
@@ -930,6 +930,262 @@ pub struct CreateTokenResponseInfo {
     pub privsep: Option<bool>,
 }
 
+const_regex! {
+
+CREATE_ZONE_EXITNODES_RE = r##"^(?i:[a-z0-9](?i:[a-z0-9\-]*[a-z0-9])?)$"##;
+CREATE_ZONE_EXITNODES_PRIMARY_RE = r##"^(?i:[a-z0-9](?i:[a-z0-9\-]*[a-z0-9])?)$"##;
+CREATE_ZONE_MAC_RE = r##"^(?i)[a-f0-9][02468ace](?::[a-f0-9]{2}){5}$"##;
+CREATE_ZONE_NODES_RE = r##"^(?i:[a-z0-9](?i:[a-z0-9\-]*[a-z0-9])?)$"##;
+CREATE_ZONE_RT_IMPORT_RE = r##"^(\d+):(\d+)$"##;
+CREATE_ZONE_ZONE_RE = r##"^[a-z][a-z0-9]*[a-z0-9]$"##;
+
+}
+
+#[api(
+    properties: {
+        "advertise-subnets": {
+            default: false,
+            optional: true,
+        },
+        bridge: {
+            optional: true,
+            type: String,
+        },
+        "bridge-disable-mac-learning": {
+            default: false,
+            optional: true,
+        },
+        controller: {
+            optional: true,
+            type: String,
+        },
+        dhcp: {
+            optional: true,
+            type: SdnZoneDhcp,
+        },
+        "disable-arp-nd-suppression": {
+            default: false,
+            optional: true,
+        },
+        dns: {
+            optional: true,
+            type: String,
+        },
+        dnszone: {
+            format: &ApiStringFormat::VerifyFn(verifiers::verify_dns_name),
+            optional: true,
+            type: String,
+        },
+        "dp-id": {
+            optional: true,
+            type: Integer,
+        },
+        exitnodes: {
+            format: &ApiStringFormat::Pattern(&CREATE_ZONE_EXITNODES_RE),
+            optional: true,
+            type: String,
+        },
+        "exitnodes-local-routing": {
+            default: false,
+            optional: true,
+        },
+        "exitnodes-primary": {
+            format: &ApiStringFormat::Pattern(&CREATE_ZONE_EXITNODES_PRIMARY_RE),
+            optional: true,
+            type: String,
+        },
+        ipam: {
+            optional: true,
+            type: String,
+        },
+        "lock-secret": {
+            optional: true,
+            type: String,
+        },
+        mac: {
+            format: &ApiStringFormat::Pattern(&CREATE_ZONE_MAC_RE),
+            optional: true,
+            type: String,
+        },
+        mtu: {
+            optional: true,
+            type: Integer,
+        },
+        nodes: {
+            format: &ApiStringFormat::Pattern(&CREATE_ZONE_NODES_RE),
+            optional: true,
+            type: String,
+        },
+        peers: {
+            format: &ApiStringFormat::VerifyFn(verifiers::verify_ip),
+            optional: true,
+            type: String,
+        },
+        reversedns: {
+            optional: true,
+            type: String,
+        },
+        "rt-import": {
+            format: &ApiStringFormat::Pattern(&CREATE_ZONE_RT_IMPORT_RE),
+            optional: true,
+            type: String,
+        },
+        tag: {
+            minimum: 0,
+            optional: true,
+            type: Integer,
+        },
+        type: {
+            type: ListZonesType,
+        },
+        "vlan-protocol": {
+            optional: true,
+            type: NetworkInterfaceVlanProtocol,
+        },
+        "vrf-vxlan": {
+            maximum: 16777215,
+            minimum: 1,
+            optional: true,
+            type: Integer,
+        },
+        "vxlan-port": {
+            maximum: 65536,
+            minimum: 1,
+            optional: true,
+            type: Integer,
+        },
+        zone: {
+            format: &ApiStringFormat::Pattern(&CREATE_ZONE_ZONE_RE),
+            type: String,
+        },
+    },
+)]
+/// Object.
+#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
+pub struct CreateZone {
+    /// Advertise IP prefixes (Type-5 routes) instead of MAC/IP pairs (Type-2
+    /// routes).
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "advertise-subnets")]
+    pub advertise_subnets: Option<bool>,
+
+    /// the bridge for which VLANs should be managed
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub bridge: Option<String>,
+
+    /// Disable auto mac learning.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "bridge-disable-mac-learning")]
+    pub bridge_disable_mac_learning: Option<bool>,
+
+    /// Controller for this zone.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub controller: Option<String>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub dhcp: Option<SdnZoneDhcp>,
+
+    /// Suppress IPv4 ARP && IPv6 Neighbour Discovery messages.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "disable-arp-nd-suppression")]
+    pub disable_arp_nd_suppression: Option<bool>,
+
+    /// dns api server
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub dns: Option<String>,
+
+    /// dns domain zone  ex: mydomain.com
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub dnszone: Option<String>,
+
+    /// Faucet dataplane id
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_i64")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "dp-id")]
+    pub dp_id: Option<i64>,
+
+    /// List of cluster node names.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub exitnodes: Option<String>,
+
+    /// Allow exitnodes to connect to EVPN guests.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "exitnodes-local-routing")]
+    pub exitnodes_local_routing: Option<bool>,
+
+    /// Force traffic through this exitnode first.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "exitnodes-primary")]
+    pub exitnodes_primary: Option<String>,
+
+    /// use a specific ipam
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub ipam: Option<String>,
+
+    /// the secret for unlocking the global SDN configuration
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "lock-secret")]
+    pub lock_secret: Option<String>,
+
+    /// Anycast logical router mac address.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub mac: Option<String>,
+
+    /// MTU of the zone, will be used for the created VNet bridges.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_i64")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub mtu: Option<i64>,
+
+    /// List of cluster node names.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub nodes: Option<String>,
+
+    /// Comma-separated list of peers, that are part of the VXLAN zone. Usually
+    /// the IPs of the nodes.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub peers: Option<String>,
+
+    /// reverse dns api server
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub reversedns: Option<String>,
+
+    /// List of Route Targets that should be imported into the VRF of the zone
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "rt-import")]
+    pub rt_import: Option<String>,
+
+    /// Service-VLAN Tag (outer VLAN)
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_u64")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub tag: Option<u64>,
+
+    #[serde(rename = "type")]
+    pub ty: ListZonesType,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "vlan-protocol")]
+    pub vlan_protocol: Option<NetworkInterfaceVlanProtocol>,
+
+    /// VNI for the zone VRF.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_u32")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "vrf-vxlan")]
+    pub vrf_vxlan: Option<u32>,
+
+    /// UDP port that should be used for the VXLAN tunnel (default 4789).
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_u32")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "vxlan-port")]
+    pub vxlan_port: Option<u32>,
+
+    /// The SDN zone object identifier.
+    pub zone: String,
+}
+
 #[api]
 /// A guest's run state.
 #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
@@ -1334,6 +1590,32 @@ pub enum ListTasksSource {
 serde_plain::derive_display_from_serialize!(ListTasksSource);
 serde_plain::derive_fromstr_from_deserialize!(ListTasksSource);
 
+#[api]
+/// Only list SDN zones of specific type
+#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
+pub enum ListZonesType {
+    #[serde(rename = "evpn")]
+    /// evpn.
+    Evpn,
+    #[serde(rename = "faucet")]
+    /// faucet.
+    Faucet,
+    #[serde(rename = "qinq")]
+    /// qinq.
+    Qinq,
+    #[serde(rename = "simple")]
+    /// simple.
+    Simple,
+    #[serde(rename = "vlan")]
+    /// vlan.
+    Vlan,
+    #[serde(rename = "vxlan")]
+    /// vxlan.
+    Vxlan,
+}
+serde_plain::derive_display_from_serialize!(ListZonesType);
+serde_plain::derive_fromstr_from_deserialize!(ListZonesType);
+
 const_regex! {
 
 LXC_CONFIG_TAGS_RE = r##"^(?i)[a-z0-9_][a-z0-9_\-+.]*$"##;
@@ -8828,6 +9110,513 @@ mod storage_info_content {
     }
 }
 
+#[api]
+/// The state of an SDN object.
+#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
+pub enum SdnObjectState {
+    #[serde(rename = "new")]
+    /// new.
+    New,
+    #[serde(rename = "deleted")]
+    /// deleted.
+    Deleted,
+    #[serde(rename = "changed")]
+    /// changed.
+    Changed,
+}
+serde_plain::derive_display_from_serialize!(SdnObjectState);
+serde_plain::derive_fromstr_from_deserialize!(SdnObjectState);
+
+const_regex! {
+
+SDN_ZONE_EXITNODES_RE = r##"^(?i:[a-z0-9](?i:[a-z0-9\-]*[a-z0-9])?)$"##;
+SDN_ZONE_EXITNODES_PRIMARY_RE = r##"^(?i:[a-z0-9](?i:[a-z0-9\-]*[a-z0-9])?)$"##;
+SDN_ZONE_RT_IMPORT_RE = r##"^(\d+):(\d+)$"##;
+
+}
+
+#[api(
+    properties: {
+        "advertise-subnets": {
+            default: false,
+            optional: true,
+        },
+        bridge: {
+            optional: true,
+            type: String,
+        },
+        "bridge-disable-mac-learning": {
+            default: false,
+            optional: true,
+        },
+        controller: {
+            optional: true,
+            type: String,
+        },
+        dhcp: {
+            optional: true,
+            type: SdnZoneDhcp,
+        },
+        digest: {
+            optional: true,
+            type: String,
+        },
+        "disable-arp-nd-suppression": {
+            default: false,
+            optional: true,
+        },
+        dns: {
+            optional: true,
+            type: String,
+        },
+        dnszone: {
+            optional: true,
+            type: String,
+        },
+        exitnodes: {
+            format: &ApiStringFormat::Pattern(&SDN_ZONE_EXITNODES_RE),
+            optional: true,
+            type: String,
+        },
+        "exitnodes-local-routing": {
+            default: false,
+            optional: true,
+        },
+        "exitnodes-primary": {
+            format: &ApiStringFormat::Pattern(&SDN_ZONE_EXITNODES_PRIMARY_RE),
+            optional: true,
+            type: String,
+        },
+        ipam: {
+            optional: true,
+            type: String,
+        },
+        mac: {
+            optional: true,
+            type: String,
+        },
+        mtu: {
+            optional: true,
+            type: Integer,
+        },
+        nodes: {
+            optional: true,
+            type: String,
+        },
+        peers: {
+            format: &ApiStringFormat::VerifyFn(verifiers::verify_ip),
+            optional: true,
+            type: String,
+        },
+        pending: {
+            optional: true,
+            type: SdnZonePending,
+        },
+        reversedns: {
+            optional: true,
+            type: String,
+        },
+        "rt-import": {
+            format: &ApiStringFormat::Pattern(&SDN_ZONE_RT_IMPORT_RE),
+            optional: true,
+            type: String,
+        },
+        state: {
+            optional: true,
+            type: SdnObjectState,
+        },
+        tag: {
+            minimum: 0,
+            optional: true,
+            type: Integer,
+        },
+        type: {
+            type: ListZonesType,
+        },
+        "vlan-protocol": {
+            optional: true,
+            type: NetworkInterfaceVlanProtocol,
+        },
+        "vrf-vxlan": {
+            maximum: 16777215,
+            minimum: 1,
+            optional: true,
+            type: Integer,
+        },
+        "vxlan-port": {
+            maximum: 65536,
+            minimum: 1,
+            optional: true,
+            type: Integer,
+        },
+        zone: {
+            type: String,
+        },
+    },
+)]
+/// Object.
+#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
+pub struct SdnZone {
+    /// Advertise IP prefixes (Type-5 routes) instead of MAC/IP pairs (Type-2
+    /// routes). EVPN zone only.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "advertise-subnets")]
+    pub advertise_subnets: Option<bool>,
+
+    /// the bridge for which VLANs should be managed. VLAN & QinQ zone only.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub bridge: Option<String>,
+
+    /// Disable auto mac learning. VLAN zone only.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "bridge-disable-mac-learning")]
+    pub bridge_disable_mac_learning: Option<bool>,
+
+    /// ID of the controller for this zone. EVPN zone only.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub controller: Option<String>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub dhcp: Option<SdnZoneDhcp>,
+
+    /// Digest of the controller section.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub digest: Option<String>,
+
+    /// Suppress IPv4 ARP && IPv6 Neighbour Discovery messages. EVPN zone only.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "disable-arp-nd-suppression")]
+    pub disable_arp_nd_suppression: Option<bool>,
+
+    /// ID of the DNS server for this zone.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub dns: Option<String>,
+
+    /// Domain name for this zone.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub dnszone: Option<String>,
+
+    /// List of PVE Nodes that should act as exit node for this zone. EVPN zone
+    /// only.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub exitnodes: Option<String>,
+
+    /// Create routes on the exit nodes, so they can connect to EVPN guests.
+    /// EVPN zone only.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "exitnodes-local-routing")]
+    pub exitnodes_local_routing: Option<bool>,
+
+    /// Force traffic through this exitnode first. EVPN zone only.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "exitnodes-primary")]
+    pub exitnodes_primary: Option<String>,
+
+    /// ID of the IPAM for this zone.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub ipam: Option<String>,
+
+    /// MAC address of the anycast router for this zone.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub mac: Option<String>,
+
+    /// MTU of the zone, will be used for the created VNet bridges.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_i64")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub mtu: Option<i64>,
+
+    /// Nodes where this zone should be created.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub nodes: Option<String>,
+
+    /// Comma-separated list of peers, that are part of the VXLAN zone. Usually
+    /// the IPs of the nodes. VXLAN zone only.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub peers: Option<String>,
+
+    /// Changes that have not yet been applied to the running configuration.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub pending: Option<SdnZonePending>,
+
+    /// ID of the reverse DNS server for this zone.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub reversedns: Option<String>,
+
+    /// Route-Targets that should be imported into the VRF of this zone via BGP.
+    /// EVPN zone only.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "rt-import")]
+    pub rt_import: Option<String>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub state: Option<SdnObjectState>,
+
+    /// Service-VLAN Tag (outer VLAN). QinQ zone only
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_u64")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub tag: Option<u64>,
+
+    #[serde(rename = "type")]
+    pub ty: ListZonesType,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "vlan-protocol")]
+    pub vlan_protocol: Option<NetworkInterfaceVlanProtocol>,
+
+    /// VNI for the zone VRF. EVPN zone only.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_u32")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "vrf-vxlan")]
+    pub vrf_vxlan: Option<u32>,
+
+    /// UDP port that should be used for the VXLAN tunnel (default 4789). VXLAN
+    /// zone only.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_u32")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "vxlan-port")]
+    pub vxlan_port: Option<u32>,
+
+    /// Name of the zone.
+    pub zone: String,
+}
+
+#[api]
+/// Name of DHCP server backend for this zone.
+#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
+pub enum SdnZoneDhcp {
+    #[serde(rename = "dnsmasq")]
+    /// dnsmasq.
+    Dnsmasq,
+}
+serde_plain::derive_display_from_serialize!(SdnZoneDhcp);
+serde_plain::derive_fromstr_from_deserialize!(SdnZoneDhcp);
+
+const_regex! {
+
+SDN_ZONE_PENDING_EXITNODES_RE = r##"^(?i:[a-z0-9](?i:[a-z0-9\-]*[a-z0-9])?)$"##;
+SDN_ZONE_PENDING_EXITNODES_PRIMARY_RE = r##"^(?i:[a-z0-9](?i:[a-z0-9\-]*[a-z0-9])?)$"##;
+SDN_ZONE_PENDING_RT_IMPORT_RE = r##"^(\d+):(\d+)$"##;
+
+}
+
+#[api(
+    properties: {
+        "advertise-subnets": {
+            default: false,
+            optional: true,
+        },
+        bridge: {
+            optional: true,
+            type: String,
+        },
+        "bridge-disable-mac-learning": {
+            default: false,
+            optional: true,
+        },
+        controller: {
+            optional: true,
+            type: String,
+        },
+        dhcp: {
+            optional: true,
+            type: SdnZoneDhcp,
+        },
+        "disable-arp-nd-suppression": {
+            default: false,
+            optional: true,
+        },
+        dns: {
+            optional: true,
+            type: String,
+        },
+        dnszone: {
+            optional: true,
+            type: String,
+        },
+        exitnodes: {
+            format: &ApiStringFormat::Pattern(&SDN_ZONE_PENDING_EXITNODES_RE),
+            optional: true,
+            type: String,
+        },
+        "exitnodes-local-routing": {
+            default: false,
+            optional: true,
+        },
+        "exitnodes-primary": {
+            format: &ApiStringFormat::Pattern(&SDN_ZONE_PENDING_EXITNODES_PRIMARY_RE),
+            optional: true,
+            type: String,
+        },
+        ipam: {
+            optional: true,
+            type: String,
+        },
+        mac: {
+            optional: true,
+            type: String,
+        },
+        mtu: {
+            optional: true,
+            type: Integer,
+        },
+        nodes: {
+            optional: true,
+            type: String,
+        },
+        peers: {
+            format: &ApiStringFormat::VerifyFn(verifiers::verify_ip),
+            optional: true,
+            type: String,
+        },
+        reversedns: {
+            optional: true,
+            type: String,
+        },
+        "rt-import": {
+            format: &ApiStringFormat::Pattern(&SDN_ZONE_PENDING_RT_IMPORT_RE),
+            optional: true,
+            type: String,
+        },
+        tag: {
+            minimum: 0,
+            optional: true,
+            type: Integer,
+        },
+        "vlan-protocol": {
+            optional: true,
+            type: NetworkInterfaceVlanProtocol,
+        },
+        "vrf-vxlan": {
+            maximum: 16777215,
+            minimum: 1,
+            optional: true,
+            type: Integer,
+        },
+        "vxlan-port": {
+            maximum: 65536,
+            minimum: 1,
+            optional: true,
+            type: Integer,
+        },
+    },
+)]
+/// Changes that have not yet been applied to the running configuration.
+#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
+pub struct SdnZonePending {
+    /// Advertise IP prefixes (Type-5 routes) instead of MAC/IP pairs (Type-2
+    /// routes). EVPN zone only.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "advertise-subnets")]
+    pub advertise_subnets: Option<bool>,
+
+    /// the bridge for which VLANs should be managed. VLAN & QinQ zone only.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub bridge: Option<String>,
+
+    /// Disable auto mac learning. VLAN zone only.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "bridge-disable-mac-learning")]
+    pub bridge_disable_mac_learning: Option<bool>,
+
+    /// ID of the controller for this zone. EVPN zone only.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub controller: Option<String>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub dhcp: Option<SdnZoneDhcp>,
+
+    /// Suppress IPv4 ARP && IPv6 Neighbour Discovery messages. EVPN zone only.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "disable-arp-nd-suppression")]
+    pub disable_arp_nd_suppression: Option<bool>,
+
+    /// ID of the DNS server for this zone.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub dns: Option<String>,
+
+    /// Domain name for this zone.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub dnszone: Option<String>,
+
+    /// List of PVE Nodes that should act as exit node for this zone. EVPN zone
+    /// only.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub exitnodes: Option<String>,
+
+    /// Create routes on the exit nodes, so they can connect to EVPN guests.
+    /// EVPN zone only.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "exitnodes-local-routing")]
+    pub exitnodes_local_routing: Option<bool>,
+
+    /// Force traffic through this exitnode first. EVPN zone only.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "exitnodes-primary")]
+    pub exitnodes_primary: Option<String>,
+
+    /// ID of the IPAM for this zone.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub ipam: Option<String>,
+
+    /// MAC address of the anycast router for this zone.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub mac: Option<String>,
+
+    /// MTU of the zone, will be used for the created VNet bridges.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_i64")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub mtu: Option<i64>,
+
+    /// Nodes where this zone should be created.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub nodes: Option<String>,
+
+    /// Comma-separated list of peers, that are part of the VXLAN zone. Usually
+    /// the IPs of the nodes. VXLAN zone only.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub peers: Option<String>,
+
+    /// ID of the reverse DNS server for this zone.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub reversedns: Option<String>,
+
+    /// Route-Targets that should be imported into the VRF of this zone via BGP.
+    /// EVPN zone only.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "rt-import")]
+    pub rt_import: Option<String>,
+
+    /// Service-VLAN Tag (outer VLAN). QinQ zone only
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_u64")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub tag: Option<u64>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "vlan-protocol")]
+    pub vlan_protocol: Option<NetworkInterfaceVlanProtocol>,
+
+    /// VNI for the zone VRF. EVPN zone only.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_u32")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "vrf-vxlan")]
+    pub vrf_vxlan: Option<u32>,
+
+    /// UDP port that should be used for the VXLAN tunnel (default 4789). VXLAN
+    /// zone only.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_u32")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "vxlan-port")]
+    pub vxlan_port: Option<u32>,
+}
+
 #[api(
     properties: {
         forceStop: {
-- 
2.39.5


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


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

* [pdm-devel] [PATCH proxmox-api-types 03/12] sdn: add list/create vnet endpoints
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 01/12] sdn: add list/create zone endpoints Stefan Hanreich
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 02/12] sdn: generate zones endpoints Stefan Hanreich
@ 2025-02-28 15:17 ` Stefan Hanreich
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 04/12] sdn: generate " Stefan Hanreich
                   ` (22 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:17 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 06baf31..d52cc3b 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');
@@ -318,6 +319,12 @@ Schema2Rust::derive('SdnZonePending' => 'Clone', 'PartialEq');
 api(POST => '/cluster/sdn/zones', 'create_zone', 'param-name' => 'CreateZone');
 Schema2Rust::derive('CreateZone' => 'Clone', 'PartialEq');
 
+api(GET => '/cluster/sdn/vnets', 'list_vnets', 'return-name' => 'SdnVnet');
+Schema2Rust::derive('SdnVnet' => 'Clone', 'PartialEq');
+Schema2Rust::derive('SdnVnetPending' => 'Clone', 'PartialEq');
+api(POST => '/cluster/sdn/vnets', 'create_vnet', 'param-name' => 'CreateVnet');
+Schema2Rust::derive('CreateVnet' => 'Clone', 'PartialEq');
+
 # NOW DUMP THE CODE:
 #
 # We generate one file for API types, and one for API method calls.
-- 
2.39.5


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


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

* [pdm-devel] [PATCH proxmox-api-types 04/12] sdn: generate vnet endpoints
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (2 preceding siblings ...)
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 03/12] sdn: add list/create vnet endpoints Stefan Hanreich
@ 2025-02-28 15:17 ` Stefan Hanreich
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 05/12] sdn: add list/create controller endpoints Stefan Hanreich
                   ` (21 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:17 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 pve-api-types/src/generated/code.rs  |  34 +++-
 pve-api-types/src/generated/types.rs | 245 +++++++++++++++++++++++++++
 2 files changed, 278 insertions(+), 1 deletion(-)

diff --git a/pve-api-types/src/generated/code.rs b/pve-api-types/src/generated/code.rs
index c1f0e74..f5a993c 100644
--- a/pve-api-types/src/generated/code.rs
+++ b/pve-api-types/src/generated/code.rs
@@ -118,7 +118,6 @@
 /// - /cluster/sdn/ipams/{ipam}/status
 /// - /cluster/sdn/lock
 /// - /cluster/sdn/rollback
-/// - /cluster/sdn/vnets
 /// - /cluster/sdn/vnets/{vnet}
 /// - /cluster/sdn/vnets/{vnet}/firewall
 /// - /cluster/sdn/vnets/{vnet}/firewall/options
@@ -411,6 +410,11 @@ pub trait PveClient {
         Err(Error::Other("create_token not implemented"))
     }
 
+    /// Create a new sdn vnet object.
+    async fn create_vnet(&self, params: CreateVnet) -> Result<(), Error> {
+        Err(Error::Other("create_vnet not implemented"))
+    }
+
     /// Create a new sdn zone object.
     async fn create_zone(&self, params: CreateZone) -> Result<(), Error> {
         Err(Error::Other("create_zone not implemented"))
@@ -489,6 +493,15 @@ pub trait PveClient {
         Err(Error::Other("list_storages not implemented"))
     }
 
+    /// SDN vnets index.
+    async fn list_vnets(
+        &self,
+        pending: Option<bool>,
+        running: Option<bool>,
+    ) -> Result<Vec<SdnVnet>, Error> {
+        Err(Error::Other("list_vnets not implemented"))
+    }
+
     /// SDN zones index.
     async fn list_zones(
         &self,
@@ -726,6 +739,12 @@ where
         Ok(self.0.post(url, &params).await?.expect_json()?.data)
     }
 
+    /// Create a new sdn vnet object.
+    async fn create_vnet(&self, params: CreateVnet) -> Result<(), Error> {
+        let url = "/api2/extjs/cluster/sdn/vnets";
+        self.0.post(url, &params).await?.nodata()
+    }
+
     /// Create a new sdn zone object.
     async fn create_zone(&self, params: CreateZone) -> Result<(), Error> {
         let url = "/api2/extjs/cluster/sdn/zones";
@@ -852,6 +871,19 @@ where
         Ok(self.0.get(url).await?.expect_json()?.data)
     }
 
+    /// SDN vnets index.
+    async fn list_vnets(
+        &self,
+        pending: Option<bool>,
+        running: Option<bool>,
+    ) -> Result<Vec<SdnVnet>, Error> {
+        let (mut query, mut sep) = (String::new(), '?');
+        add_query_bool(&mut query, &mut sep, "pending", pending);
+        add_query_bool(&mut query, &mut sep, "running", running);
+        let url = &format!("/api2/extjs/cluster/sdn/vnets{query}");
+        Ok(self.0.get(url).await?.expect_json()?.data)
+    }
+
     /// SDN zones index.
     async fn list_zones(
         &self,
diff --git a/pve-api-types/src/generated/types.rs b/pve-api-types/src/generated/types.rs
index 408d8ca..871707a 100644
--- a/pve-api-types/src/generated/types.rs
+++ b/pve-api-types/src/generated/types.rs
@@ -932,6 +932,91 @@ pub struct CreateTokenResponseInfo {
 
 const_regex! {
 
+CREATE_VNET_VNET_RE = r##"^[a-z][a-z0-9]*[a-z0-9]$"##;
+
+}
+
+#[api(
+    properties: {
+        alias: {
+            max_length: 256,
+            optional: true,
+            type: String,
+        },
+        "isolate-ports": {
+            default: false,
+            optional: true,
+        },
+        "lock-secret": {
+            optional: true,
+            type: String,
+        },
+        tag: {
+            maximum: 16777215,
+            minimum: 1,
+            optional: true,
+            type: Integer,
+        },
+        type: {
+            optional: true,
+            type: SdnVnetType,
+        },
+        vlanaware: {
+            default: false,
+            optional: true,
+        },
+        vnet: {
+            format: &ApiStringFormat::Pattern(&CREATE_VNET_VNET_RE),
+            type: String,
+        },
+        zone: {
+            type: String,
+        },
+    },
+)]
+/// Object.
+#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
+pub struct CreateVnet {
+    /// Alias name of the VNet.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub alias: Option<String>,
+
+    /// If true, sets the isolated property for all interfaces on the bridge of
+    /// this VNet.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "isolate-ports")]
+    pub isolate_ports: Option<bool>,
+
+    /// the secret for unlocking the global SDN configuration
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "lock-secret")]
+    pub lock_secret: Option<String>,
+
+    /// VLAN Tag (for VLAN or QinQ zones) or VXLAN VNI (for VXLAN or EVPN
+    /// zones).
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_u32")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub tag: Option<u32>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "type")]
+    pub ty: Option<SdnVnetType>,
+
+    /// Allow VLANs to pass through this vnet.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub vlanaware: Option<bool>,
+
+    /// The SDN vnet object identifier.
+    pub vnet: String,
+
+    /// Name of the zone this VNet belongs to.
+    pub zone: String,
+}
+
+const_regex! {
+
 CREATE_ZONE_EXITNODES_RE = r##"^(?i:[a-z0-9](?i:[a-z0-9\-]*[a-z0-9])?)$"##;
 CREATE_ZONE_EXITNODES_PRIMARY_RE = r##"^(?i:[a-z0-9](?i:[a-z0-9\-]*[a-z0-9])?)$"##;
 CREATE_ZONE_MAC_RE = r##"^(?i)[a-f0-9][02468ace](?::[a-f0-9]{2}){5}$"##;
@@ -9127,6 +9212,166 @@ pub enum SdnObjectState {
 serde_plain::derive_display_from_serialize!(SdnObjectState);
 serde_plain::derive_fromstr_from_deserialize!(SdnObjectState);
 
+#[api(
+    properties: {
+        alias: {
+            max_length: 256,
+            optional: true,
+            type: String,
+        },
+        digest: {
+            optional: true,
+            type: String,
+        },
+        "isolate-ports": {
+            default: false,
+            optional: true,
+        },
+        pending: {
+            optional: true,
+            type: SdnVnetPending,
+        },
+        state: {
+            optional: true,
+            type: SdnObjectState,
+        },
+        tag: {
+            maximum: 16777215,
+            minimum: 1,
+            optional: true,
+            type: Integer,
+        },
+        type: {
+            type: SdnVnetType,
+        },
+        vlanaware: {
+            default: false,
+            optional: true,
+        },
+        vnet: {
+            type: String,
+        },
+        zone: {
+            optional: true,
+            type: String,
+        },
+    },
+)]
+/// Object.
+#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
+pub struct SdnVnet {
+    /// Alias name of the VNet.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub alias: Option<String>,
+
+    /// Digest of the VNet section.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub digest: Option<String>,
+
+    /// If true, sets the isolated property for all interfaces on the bridge of
+    /// this VNet.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "isolate-ports")]
+    pub isolate_ports: Option<bool>,
+
+    /// Changes that have not yet been applied to the running configuration.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub pending: Option<SdnVnetPending>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub state: Option<SdnObjectState>,
+
+    /// VLAN Tag (for VLAN or QinQ zones) or VXLAN VNI (for VXLAN or EVPN
+    /// zones).
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_u32")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub tag: Option<u32>,
+
+    #[serde(rename = "type")]
+    pub ty: SdnVnetType,
+
+    /// Allow VLANs to pass through this VNet.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub vlanaware: Option<bool>,
+
+    /// Name of the VNet.
+    pub vnet: String,
+
+    /// Name of the zone this VNet belongs to.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub zone: Option<String>,
+}
+
+#[api(
+    properties: {
+        alias: {
+            max_length: 256,
+            optional: true,
+            type: String,
+        },
+        "isolate-ports": {
+            default: false,
+            optional: true,
+        },
+        tag: {
+            maximum: 16777215,
+            minimum: 1,
+            optional: true,
+            type: Integer,
+        },
+        vlanaware: {
+            default: false,
+            optional: true,
+        },
+        zone: {
+            optional: true,
+            type: String,
+        },
+    },
+)]
+/// Changes that have not yet been applied to the running configuration.
+#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
+pub struct SdnVnetPending {
+    /// Alias name of the VNet.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub alias: Option<String>,
+
+    /// If true, sets the isolated property for all interfaces on the bridge of
+    /// this VNet.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "isolate-ports")]
+    pub isolate_ports: Option<bool>,
+
+    /// VLAN Tag (for VLAN or QinQ zones) or VXLAN VNI (for VXLAN or EVPN
+    /// zones).
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_u32")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub tag: Option<u32>,
+
+    /// Allow VLANs to pass through this VNet.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub vlanaware: Option<bool>,
+
+    /// Name of the zone this VNet belongs to.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub zone: Option<String>,
+}
+
+#[api]
+/// Type of the VNet.
+#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
+pub enum SdnVnetType {
+    #[serde(rename = "vnet")]
+    /// vnet.
+    Vnet,
+}
+serde_plain::derive_display_from_serialize!(SdnVnetType);
+serde_plain::derive_fromstr_from_deserialize!(SdnVnetType);
+
 const_regex! {
 
 SDN_ZONE_EXITNODES_RE = r##"^(?i:[a-z0-9](?i:[a-z0-9\-]*[a-z0-9])?)$"##;
-- 
2.39.5


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


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

* [pdm-devel] [PATCH proxmox-api-types 05/12] sdn: add list/create controller endpoints
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (3 preceding siblings ...)
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 04/12] sdn: generate " Stefan Hanreich
@ 2025-02-28 15:17 ` Stefan Hanreich
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 06/12] sdn: generate " Stefan Hanreich
                   ` (20 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:17 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 d52cc3b..6e9b3f4 100644
--- a/pve-api-types/generate.pl
+++ b/pve-api-types/generate.pl
@@ -319,6 +319,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.39.5


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


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

* [pdm-devel] [PATCH proxmox-api-types 06/12] sdn: generate controller endpoints
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (4 preceding siblings ...)
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 05/12] sdn: add list/create controller endpoints Stefan Hanreich
@ 2025-02-28 15:17 ` Stefan Hanreich
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 07/12] sdn: add acquire/release lock endpoints Stefan Hanreich
                   ` (19 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:17 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 pve-api-types/src/generated/code.rs  |  37 ++-
 pve-api-types/src/generated/types.rs | 413 +++++++++++++++++++++++++++
 2 files changed, 449 insertions(+), 1 deletion(-)

diff --git a/pve-api-types/src/generated/code.rs b/pve-api-types/src/generated/code.rs
index f5a993c..5283df4 100644
--- a/pve-api-types/src/generated/code.rs
+++ b/pve-api-types/src/generated/code.rs
@@ -109,7 +109,6 @@
 /// - /cluster/replication
 /// - /cluster/replication/{id}
 /// - /cluster/sdn
-/// - /cluster/sdn/controllers
 /// - /cluster/sdn/controllers/{controller}
 /// - /cluster/sdn/dns
 /// - /cluster/sdn/dns/{dns}
@@ -399,6 +398,11 @@ pub trait PveClient {
         Err(Error::Other("cluster_status not implemented"))
     }
 
+    /// Create a new sdn controller object.
+    async fn create_controller(&self, params: CreateController) -> Result<(), Error> {
+        Err(Error::Other("create_controller not implemented"))
+    }
+
     /// Generate a new API token for a specific user. NOTE: returns API token
     /// value, which needs to be stored as it cannot be retrieved afterwards!
     async fn create_token(
@@ -451,6 +455,16 @@ pub trait PveClient {
         Err(Error::Other("get_task_status not implemented"))
     }
 
+    /// SDN controllers index.
+    async fn list_controllers(
+        &self,
+        pending: Option<bool>,
+        running: Option<bool>,
+        ty: Option<ListControllersType>,
+    ) -> Result<Vec<SdnController>, Error> {
+        Err(Error::Other("list_controllers not implemented"))
+    }
+
     /// Authentication domain index.
     async fn list_domains(&self) -> Result<Vec<ListRealm>, Error> {
         Err(Error::Other("list_domains not implemented"))
@@ -727,6 +741,12 @@ where
         Ok(self.0.get(url).await?.expect_json()?.data)
     }
 
+    /// Create a new sdn controller object.
+    async fn create_controller(&self, params: CreateController) -> Result<(), Error> {
+        let url = "/api2/extjs/cluster/sdn/controllers";
+        self.0.post(url, &params).await?.nodata()
+    }
+
     /// Generate a new API token for a specific user. NOTE: returns API token
     /// value, which needs to be stored as it cannot be retrieved afterwards!
     async fn create_token(
@@ -813,6 +833,21 @@ where
         Ok(self.0.get(url).await?.expect_json()?.data)
     }
 
+    /// SDN controllers index.
+    async fn list_controllers(
+        &self,
+        pending: Option<bool>,
+        running: Option<bool>,
+        ty: Option<ListControllersType>,
+    ) -> Result<Vec<SdnController>, Error> {
+        let (mut query, mut sep) = (String::new(), '?');
+        add_query_bool(&mut query, &mut sep, "pending", pending);
+        add_query_bool(&mut query, &mut sep, "running", running);
+        add_query_arg(&mut query, &mut sep, "type", &ty);
+        let url = &format!("/api2/extjs/cluster/sdn/controllers{query}");
+        Ok(self.0.get(url).await?.expect_json()?.data)
+    }
+
     /// Authentication domain index.
     async fn list_domains(&self) -> Result<Vec<ListRealm>, Error> {
         let url = "/api2/extjs/access/domains";
diff --git a/pve-api-types/src/generated/types.rs b/pve-api-types/src/generated/types.rs
index 871707a..92dc968 100644
--- a/pve-api-types/src/generated/types.rs
+++ b/pve-api-types/src/generated/types.rs
@@ -830,6 +830,140 @@ pub enum ClusterResourceType {
 serde_plain::derive_display_from_serialize!(ClusterResourceType);
 serde_plain::derive_fromstr_from_deserialize!(ClusterResourceType);
 
+const_regex! {
+
+CREATE_CONTROLLER_CONTROLLER_RE = r##"^[a-z][a-z0-9_-]*[a-z0-9]$"##;
+CREATE_CONTROLLER_ISIS_IFACES_RE = r##"^[a-zA-Z][a-zA-Z0-9_]{1,20}([:\.]\d+)?$"##;
+CREATE_CONTROLLER_ISIS_NET_RE = r##"^[a-fA-F0-9]{2}(\.[a-fA-F0-9]{4}){3,9}\.[a-fA-F0-9]{2}$"##;
+CREATE_CONTROLLER_NODE_RE = r##"^(?i:[a-z0-9](?i:[a-z0-9\-]*[a-z0-9])?)$"##;
+
+}
+
+#[api(
+    properties: {
+        asn: {
+            maximum: 4294967295,
+            minimum: 0,
+            optional: true,
+            type: Integer,
+        },
+        "bgp-multipath-as-path-relax": {
+            default: false,
+            optional: true,
+        },
+        controller: {
+            format: &ApiStringFormat::Pattern(&CREATE_CONTROLLER_CONTROLLER_RE),
+            type: String,
+        },
+        ebgp: {
+            default: false,
+            optional: true,
+        },
+        "ebgp-multihop": {
+            optional: true,
+            type: Integer,
+        },
+        "isis-domain": {
+            optional: true,
+            type: String,
+        },
+        "isis-ifaces": {
+            format: &ApiStringFormat::Pattern(&CREATE_CONTROLLER_ISIS_IFACES_RE),
+            optional: true,
+            type: String,
+        },
+        "isis-net": {
+            format: &ApiStringFormat::Pattern(&CREATE_CONTROLLER_ISIS_NET_RE),
+            optional: true,
+            type: String,
+        },
+        "lock-secret": {
+            optional: true,
+            type: String,
+        },
+        loopback: {
+            optional: true,
+            type: String,
+        },
+        node: {
+            format: &ApiStringFormat::Pattern(&CREATE_CONTROLLER_NODE_RE),
+            optional: true,
+            type: String,
+        },
+        peers: {
+            format: &ApiStringFormat::VerifyFn(verifiers::verify_ip),
+            optional: true,
+            type: String,
+        },
+        type: {
+            type: ListControllersType,
+        },
+    },
+)]
+/// Object.
+#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
+pub struct CreateController {
+    /// autonomous system number
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_u32")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub asn: Option<u32>,
+
+    /// Consider different AS paths of equal length for multipath computation.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "bgp-multipath-as-path-relax")]
+    pub bgp_multipath_as_path_relax: Option<bool>,
+
+    /// The SDN controller object identifier.
+    pub controller: String,
+
+    /// Enable eBGP (remote-as external).
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub ebgp: Option<bool>,
+
+    /// Set maximum amount of hops for eBGP peers.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_i64")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "ebgp-multihop")]
+    pub ebgp_multihop: Option<i64>,
+
+    /// Name of the IS-IS domain.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "isis-domain")]
+    pub isis_domain: Option<String>,
+
+    /// Comma-separated list of interfaces where IS-IS should be active.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "isis-ifaces")]
+    pub isis_ifaces: Option<String>,
+
+    /// Network Entity title for this node in the IS-IS network.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "isis-net")]
+    pub isis_net: Option<String>,
+
+    /// the secret for unlocking the global SDN configuration
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "lock-secret")]
+    pub lock_secret: Option<String>,
+
+    /// Name of the loopback/dummy interface that provides the Router-IP.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub loopback: Option<String>,
+
+    /// The cluster node name.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub node: Option<String>,
+
+    /// peers address list.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub peers: Option<String>,
+
+    #[serde(rename = "type")]
+    pub ty: ListControllersType,
+}
+
 #[api(
     properties: {
         comment: {
@@ -1380,6 +1514,26 @@ mod list_storages_content {
     }
 }
 
+#[api]
+/// Only list sdn controllers of specific type
+#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
+pub enum ListControllersType {
+    #[serde(rename = "bgp")]
+    /// bgp.
+    Bgp,
+    #[serde(rename = "evpn")]
+    /// evpn.
+    Evpn,
+    #[serde(rename = "faucet")]
+    /// faucet.
+    Faucet,
+    #[serde(rename = "isis")]
+    /// isis.
+    Isis,
+}
+serde_plain::derive_display_from_serialize!(ListControllersType);
+serde_plain::derive_fromstr_from_deserialize!(ListControllersType);
+
 #[api]
 /// Only list specific interface types.
 #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
@@ -9195,6 +9349,265 @@ mod storage_info_content {
     }
 }
 
+const_regex! {
+
+SDN_CONTROLLER_ISIS_IFACES_RE = r##"^[a-zA-Z][a-zA-Z0-9_]{1,20}([:\.]\d+)?$"##;
+SDN_CONTROLLER_ISIS_NET_RE = r##"^[a-fA-F0-9]{2}(\.[a-fA-F0-9]{4}){3,9}\.[a-fA-F0-9]{2}$"##;
+
+}
+
+#[api(
+    properties: {
+        asn: {
+            maximum: 4294967295,
+            minimum: 0,
+            optional: true,
+            type: Integer,
+        },
+        "bgp-multipath-as-relax": {
+            default: false,
+            optional: true,
+        },
+        controller: {
+            type: String,
+        },
+        digest: {
+            optional: true,
+            type: String,
+        },
+        ebgp: {
+            default: false,
+            optional: true,
+        },
+        "ebgp-multihop": {
+            optional: true,
+            type: Integer,
+        },
+        "isis-domain": {
+            optional: true,
+            type: String,
+        },
+        "isis-ifaces": {
+            format: &ApiStringFormat::Pattern(&SDN_CONTROLLER_ISIS_IFACES_RE),
+            optional: true,
+            type: String,
+        },
+        "isis-net": {
+            format: &ApiStringFormat::Pattern(&SDN_CONTROLLER_ISIS_NET_RE),
+            optional: true,
+            type: String,
+        },
+        loopback: {
+            optional: true,
+            type: String,
+        },
+        node: {
+            optional: true,
+            type: String,
+        },
+        peers: {
+            optional: true,
+            type: String,
+        },
+        pending: {
+            optional: true,
+            type: SdnControllerPending,
+        },
+        state: {
+            optional: true,
+            type: SdnObjectState,
+        },
+        type: {
+            type: ListControllersType,
+        },
+    },
+)]
+/// Object.
+#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
+pub struct SdnController {
+    /// The local ASN of the controller. BGP & EVPN only.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_u32")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub asn: Option<u32>,
+
+    /// Consider different AS paths of equal length for multipath computation.
+    /// BGP only.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "bgp-multipath-as-relax")]
+    pub bgp_multipath_as_relax: Option<bool>,
+
+    /// Name of the controller.
+    pub controller: String,
+
+    /// Digest of the controller section.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub digest: Option<String>,
+
+    /// Enable eBGP (remote-as external). BGP only.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub ebgp: Option<bool>,
+
+    /// Set maximum amount of hops for eBGP peers. Needs ebgp set to 1. BGP
+    /// only.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_i64")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "ebgp-multihop")]
+    pub ebgp_multihop: Option<i64>,
+
+    /// Name of the IS-IS domain. IS-IS only.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "isis-domain")]
+    pub isis_domain: Option<String>,
+
+    /// Comma-separated list of interfaces where IS-IS should be active. IS-IS
+    /// only.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "isis-ifaces")]
+    pub isis_ifaces: Option<String>,
+
+    /// Network Entity title for this node in the IS-IS network. IS-IS only.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "isis-net")]
+    pub isis_net: Option<String>,
+
+    /// Name of the loopback/dummy interface that provides the Router-IP. BGP
+    /// only.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub loopback: Option<String>,
+
+    /// Node(s) where this controller is active.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub node: Option<String>,
+
+    /// Comma-separated list of the peers IP addresses.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub peers: Option<String>,
+
+    /// Changes that have not yet been applied to the running configuration.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub pending: Option<SdnControllerPending>,
+
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub state: Option<SdnObjectState>,
+
+    #[serde(rename = "type")]
+    pub ty: ListControllersType,
+}
+
+const_regex! {
+
+SDN_CONTROLLER_PENDING_ISIS_IFACES_RE = r##"^[a-zA-Z][a-zA-Z0-9_]{1,20}([:\.]\d+)?$"##;
+SDN_CONTROLLER_PENDING_ISIS_NET_RE = r##"^[a-fA-F0-9]{2}(\.[a-fA-F0-9]{4}){3,9}\.[a-fA-F0-9]{2}$"##;
+
+}
+
+#[api(
+    properties: {
+        asn: {
+            maximum: 4294967295,
+            minimum: 0,
+            optional: true,
+            type: Integer,
+        },
+        "bgp-multipath-as-relax": {
+            default: false,
+            optional: true,
+        },
+        ebgp: {
+            default: false,
+            optional: true,
+        },
+        "ebgp-multihop": {
+            optional: true,
+            type: Integer,
+        },
+        "isis-domain": {
+            optional: true,
+            type: String,
+        },
+        "isis-ifaces": {
+            format: &ApiStringFormat::Pattern(&SDN_CONTROLLER_PENDING_ISIS_IFACES_RE),
+            optional: true,
+            type: String,
+        },
+        "isis-net": {
+            format: &ApiStringFormat::Pattern(&SDN_CONTROLLER_PENDING_ISIS_NET_RE),
+            optional: true,
+            type: String,
+        },
+        loopback: {
+            optional: true,
+            type: String,
+        },
+        node: {
+            optional: true,
+            type: String,
+        },
+        peers: {
+            optional: true,
+            type: String,
+        },
+    },
+)]
+/// Changes that have not yet been applied to the running configuration.
+#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
+pub struct SdnControllerPending {
+    /// The local ASN of the controller. BGP & EVPN only.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_u32")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub asn: Option<u32>,
+
+    /// Consider different AS paths of equal length for multipath computation.
+    /// BGP only.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "bgp-multipath-as-relax")]
+    pub bgp_multipath_as_relax: Option<bool>,
+
+    /// Enable eBGP (remote-as external). BGP only.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub ebgp: Option<bool>,
+
+    /// Set maximum amount of hops for eBGP peers. Needs ebgp set to 1. BGP
+    /// only.
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_i64")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "ebgp-multihop")]
+    pub ebgp_multihop: Option<i64>,
+
+    /// Name of the IS-IS domain. IS-IS only.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "isis-domain")]
+    pub isis_domain: Option<String>,
+
+    /// Comma-separated list of interfaces where IS-IS should be active. IS-IS
+    /// only.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "isis-ifaces")]
+    pub isis_ifaces: Option<String>,
+
+    /// Network Entity title for this node in the IS-IS network. IS-IS only.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "isis-net")]
+    pub isis_net: Option<String>,
+
+    /// Name of the loopback/dummy interface that provides the Router-IP. BGP
+    /// only.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub loopback: Option<String>,
+
+    /// Node(s) where this controller is active.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub node: Option<String>,
+
+    /// Comma-separated list of the peers IP addresses.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub peers: Option<String>,
+}
+
 #[api]
 /// The state of an SDN object.
 #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
-- 
2.39.5


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


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

* [pdm-devel] [PATCH proxmox-api-types 07/12] sdn: add acquire/release lock endpoints
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (5 preceding siblings ...)
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 06/12] sdn: generate " Stefan Hanreich
@ 2025-02-28 15:17 ` Stefan Hanreich
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 08/12] sdn: generate " Stefan Hanreich
                   ` (18 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:17 UTC (permalink / raw)
  To: pdm-devel

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

diff --git a/pve-api-types/generate.pl b/pve-api-types/generate.pl
index 6e9b3f4..449200e 100644
--- a/pve-api-types/generate.pl
+++ b/pve-api-types/generate.pl
@@ -331,6 +331,9 @@ 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');
+
 # NOW DUMP THE CODE:
 #
 # We generate one file for API types, and one for API method calls.
-- 
2.39.5


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


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

* [pdm-devel] [PATCH proxmox-api-types 08/12] sdn: generate lock endpoints
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (6 preceding siblings ...)
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 07/12] sdn: add acquire/release lock endpoints Stefan Hanreich
@ 2025-02-28 15:17 ` Stefan Hanreich
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 09/12] sdn: add apply configuration endpoint Stefan Hanreich
                   ` (17 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:17 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 pve-api-types/src/generated/code.rs  | 30 ++++++++++++++++++-
 pve-api-types/src/generated/types.rs | 44 ++++++++++++++++++++++++++++
 2 files changed, 73 insertions(+), 1 deletion(-)

diff --git a/pve-api-types/src/generated/code.rs b/pve-api-types/src/generated/code.rs
index 5283df4..7927ccb 100644
--- a/pve-api-types/src/generated/code.rs
+++ b/pve-api-types/src/generated/code.rs
@@ -115,7 +115,6 @@
 /// - /cluster/sdn/ipams
 /// - /cluster/sdn/ipams/{ipam}
 /// - /cluster/sdn/ipams/{ipam}/status
-/// - /cluster/sdn/lock
 /// - /cluster/sdn/rollback
 /// - /cluster/sdn/vnets/{vnet}
 /// - /cluster/sdn/vnets/{vnet}/firewall
@@ -370,6 +369,11 @@
 /// ```
 #[async_trait::async_trait]
 pub trait PveClient {
+    /// Acquire global lock for SDN configuration
+    async fn acquire_sdn_lock(&self, params: CreateSdnLock) -> Result<String, Error> {
+        Err(Error::Other("acquire_sdn_lock not implemented"))
+    }
+
     /// Get information needed to join this cluster over the connected node.
     async fn cluster_config_join(&self, node: Option<String>) -> Result<ClusterJoinInfo, Error> {
         Err(Error::Other("cluster_config_join not implemented"))
@@ -595,6 +599,11 @@ pub trait PveClient {
         Err(Error::Other("qemu_migrate_preconditions not implemented"))
     }
 
+    /// Release global lock for SDN configuration
+    async fn release_sdn_lock(&self, params: ReleaseSdnLock) -> Result<(), Error> {
+        Err(Error::Other("release_sdn_lock not implemented"))
+    }
+
     /// Migrate the container to another cluster. Creates a new migration task.
     /// EXPERIMENTAL feature!
     async fn remote_migrate_lxc(
@@ -701,6 +710,12 @@ where
     T: HttpApiClient + Send + Sync,
     for<'a> <T as HttpApiClient>::ResponseFuture<'a>: Send,
 {
+    /// Acquire global lock for SDN configuration
+    async fn acquire_sdn_lock(&self, params: CreateSdnLock) -> Result<String, Error> {
+        let url = "/api2/extjs/cluster/sdn/lock";
+        Ok(self.0.post(url, &params).await?.expect_json()?.data)
+    }
+
     /// Get information needed to join this cluster over the connected node.
     async fn cluster_config_join(&self, node: Option<String>) -> Result<ClusterJoinInfo, Error> {
         let (mut query, mut sep) = (String::new(), '?');
@@ -1019,6 +1034,19 @@ where
         Ok(self.0.get(url).await?.expect_json()?.data)
     }
 
+    /// Release global lock for SDN configuration
+    async fn release_sdn_lock(&self, params: ReleaseSdnLock) -> Result<(), Error> {
+        let (mut query, mut sep) = (String::new(), '?');
+        let ReleaseSdnLock {
+            force: p_force,
+            lock_secret: p_lock_secret,
+        } = params;
+        add_query_bool(&mut query, &mut sep, "force", p_force);
+        add_query_arg(&mut query, &mut sep, "lock-secret", &p_lock_secret);
+        let url = &format!("/api2/extjs/cluster/sdn/lock{query}");
+        self.0.delete(url).await?.nodata()
+    }
+
     /// Migrate the container to another cluster. Creates a new migration task.
     /// EXPERIMENTAL feature!
     async fn remote_migrate_lxc(
diff --git a/pve-api-types/src/generated/types.rs b/pve-api-types/src/generated/types.rs
index 92dc968..78fdbd1 100644
--- a/pve-api-types/src/generated/types.rs
+++ b/pve-api-types/src/generated/types.rs
@@ -964,6 +964,24 @@ pub struct CreateController {
     pub ty: ListControllersType,
 }
 
+#[api(
+    properties: {
+        "allow-pending": {
+            default: false,
+            optional: true,
+        },
+    },
+)]
+/// Object.
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct CreateSdnLock {
+    /// if true, allow acquiring lock even though there are pending changes
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "allow-pending")]
+    pub allow_pending: Option<bool>,
+}
+
 #[api(
     properties: {
         comment: {
@@ -9080,6 +9098,32 @@ pub struct QemuStatus {
     pub vmid: u32,
 }
 
+#[api(
+    properties: {
+        force: {
+            default: false,
+            optional: true,
+        },
+        "lock-secret": {
+            optional: true,
+            type: String,
+        },
+    },
+)]
+/// Object.
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct ReleaseSdnLock {
+    /// if true, allow releasing lock without providing the secret
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub force: Option<bool>,
+
+    /// the secret for unlocking the global SDN configuration
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "lock-secret")]
+    pub lock_secret: Option<String>,
+}
+
 #[api(
     properties: {
         bwlimit: {
-- 
2.39.5


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


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

* [pdm-devel] [PATCH proxmox-api-types 09/12] sdn: add apply configuration endpoint
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (7 preceding siblings ...)
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 08/12] sdn: generate " Stefan Hanreich
@ 2025-02-28 15:17 ` Stefan Hanreich
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 10/12] sdn: generate " Stefan Hanreich
                   ` (16 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:17 UTC (permalink / raw)
  To: pdm-devel

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 449200e..3101b41 100644
--- a/pve-api-types/generate.pl
+++ b/pve-api-types/generate.pl
@@ -334,6 +334,8 @@ 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');
+
 # NOW DUMP THE CODE:
 #
 # We generate one file for API types, and one for API method calls.
-- 
2.39.5


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


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

* [pdm-devel] [PATCH proxmox-api-types 10/12] sdn: generate apply configuration endpoint
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (8 preceding siblings ...)
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 09/12] sdn: add apply configuration endpoint Stefan Hanreich
@ 2025-02-28 15:17 ` Stefan Hanreich
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 11/12] tasks: add helper for querying successfully finished tasks Stefan Hanreich
                   ` (15 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:17 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 pve-api-types/src/generated/code.rs  | 12 +++++++++++-
 pve-api-types/src/generated/types.rs | 28 ++++++++++++++++++++++++++++
 2 files changed, 39 insertions(+), 1 deletion(-)

diff --git a/pve-api-types/src/generated/code.rs b/pve-api-types/src/generated/code.rs
index 7927ccb..2e610b3 100644
--- a/pve-api-types/src/generated/code.rs
+++ b/pve-api-types/src/generated/code.rs
@@ -108,7 +108,6 @@
 /// - /cluster/options
 /// - /cluster/replication
 /// - /cluster/replication/{id}
-/// - /cluster/sdn
 /// - /cluster/sdn/controllers/{controller}
 /// - /cluster/sdn/dns
 /// - /cluster/sdn/dns/{dns}
@@ -626,6 +625,11 @@ pub trait PveClient {
         Err(Error::Other("remote_migrate_qemu not implemented"))
     }
 
+    /// Apply sdn controller changes && reload.
+    async fn sdn_apply(&self, params: ReloadSdn) -> Result<PveUpid, Error> {
+        Err(Error::Other("sdn_apply not implemented"))
+    }
+
     /// Shutdown the container. This will trigger a clean shutdown of the
     /// container, see lxc-stop(1) for details.
     async fn shutdown_lxc_async(
@@ -1071,6 +1075,12 @@ where
         Ok(self.0.post(url, &params).await?.expect_json()?.data)
     }
 
+    /// Apply sdn controller changes && reload.
+    async fn sdn_apply(&self, params: ReloadSdn) -> Result<PveUpid, Error> {
+        let url = "/api2/extjs/cluster/sdn";
+        Ok(self.0.put(url, &params).await?.expect_json()?.data)
+    }
+
     /// Shutdown the container. This will trigger a clean shutdown of the
     /// container, see lxc-stop(1) for details.
     async fn shutdown_lxc_async(
diff --git a/pve-api-types/src/generated/types.rs b/pve-api-types/src/generated/types.rs
index 78fdbd1..2fce1ef 100644
--- a/pve-api-types/src/generated/types.rs
+++ b/pve-api-types/src/generated/types.rs
@@ -9124,6 +9124,34 @@ pub struct ReleaseSdnLock {
     pub lock_secret: Option<String>,
 }
 
+#[api(
+    properties: {
+        "lock-secret": {
+            optional: true,
+            type: String,
+        },
+        "release-lock": {
+            default: true,
+            optional: true,
+        },
+    },
+)]
+/// Object.
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct ReloadSdn {
+    /// the secret for unlocking the global SDN configuration
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "lock-secret")]
+    pub lock_secret: Option<String>,
+
+    /// When lock-secret has been provided and configuration successfully
+    /// commited, release the lock automatically afterwards
+    #[serde(deserialize_with = "proxmox_login::parse::deserialize_bool")]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(rename = "release-lock")]
+    pub release_lock: Option<bool>,
+}
+
 #[api(
     properties: {
         bwlimit: {
-- 
2.39.5


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


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

* [pdm-devel] [PATCH proxmox-api-types 11/12] tasks: add helper for querying successfully finished tasks
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (9 preceding siblings ...)
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 10/12] sdn: generate " Stefan Hanreich
@ 2025-02-28 15:17 ` Stefan Hanreich
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 12/12] sdn: add helpers for pending values Stefan Hanreich
                   ` (14 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:17 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 e8f5c86..ef4abac 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.39.5


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


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

* [pdm-devel] [PATCH proxmox-api-types 12/12] sdn: add helpers for pending values
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (10 preceding siblings ...)
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 11/12] tasks: add helper for querying successfully finished tasks Stefan Hanreich
@ 2025-02-28 15:17 ` Stefan Hanreich
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-yew-comp 1/1] sdn: add descriptions for sdn tasks Stefan Hanreich
                   ` (13 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:17 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.39.5


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


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

* [pdm-devel] [PATCH proxmox-yew-comp 1/1] sdn: add descriptions for sdn tasks
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (11 preceding siblings ...)
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 12/12] sdn: add helpers for pending values Stefan Hanreich
@ 2025-02-28 15:17 ` Stefan Hanreich
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 01/13] server: add locked sdn client and helper methods Stefan Hanreich
                   ` (12 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:17 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 d500b6d..d0ade32 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!("Setrvice"), 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.39.5


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


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

* [pdm-devel] [PATCH proxmox-datacenter-manager 01/13] server: add locked sdn client and helper methods
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (12 preceding siblings ...)
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-yew-comp 1/1] sdn: add descriptions for sdn tasks Stefan Hanreich
@ 2025-02-28 15:17 ` Stefan Hanreich
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 02/13] api: sdn: add list_zones endpoint Stefan Hanreich
                   ` (11 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:17 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.

There are also helpers included that make writing code that tries to
connect to multiple remotes simultaneously easier. This will be
particularly useful for the API methods that are manipulating the SDN
configuration across multiple remotes.

For more information on how they work, please consult the
documentation of the struct, its methods and the helper methods.

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

diff --git a/server/src/lib.rs b/server/src/lib.rs
index 12dc912..45eee84 100644
--- a/server/src/lib.rs
+++ b/server/src/lib.rs
@@ -12,6 +12,7 @@ pub mod task_utils;
 
 pub mod connection;
 pub mod pbs_client;
+pub mod sdn_client;
 
 #[cfg(remote_config = "faked")]
 pub mod test_support;
diff --git a/server/src/sdn_client.rs b/server/src/sdn_client.rs
new file mode 100644
index 0000000..fd77305
--- /dev/null
+++ b/server/src/sdn_client.rs
@@ -0,0 +1,234 @@
+use std::{collections::HashMap, time::Duration};
+
+use anyhow::{self, bail};
+
+use futures::{stream::FuturesUnordered, StreamExt, TryFutureExt};
+use pdm_api_types::{remotes::Remote, RemoteUpid};
+use pve_api_types::{
+    client::PveClient, CreateSdnLock, CreateVnet, CreateZone, PveUpid, ReleaseSdnLock, ReloadSdn,
+};
+
+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_secret that is required for
+/// making changes to the SDN configuration. It provides methods that proxy the respective SDN
+/// endpoints, where it adds the lock_secret when making the proxied calls.
+pub struct LockedSdnClient {
+    secret: String,
+    client: Box<dyn PveClient + Send + Sync>,
+}
+
+impl LockedSdnClient {
+    /// Consumes a [`PveClient`] and locks the remote instance. On success, returns a new
+    /// [`LockedSdnClient`] where the remotes' SDN configuration has been locked.
+    ///
+    /// # 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_secret to the passed parameters before
+    /// making the call.
+    pub async fn create_vnet(&self, mut params: CreateVnet) -> Result<(), proxmox_client::Error> {
+        params.lock_secret = Some(self.secret.clone());
+
+        self.client.create_vnet(params).await
+    }
+
+    /// proxies [`PveClient::create_zone`] and adds lock_secret to the passed parameters before
+    /// making the call.
+    pub async fn create_zone(&self, mut params: CreateZone) -> Result<(), proxmox_client::Error> {
+        params.lock_secret = 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, Box<dyn PveClient + Send + Sync>), proxmox_client::Error> {
+        let params = ReloadSdn {
+            lock_secret: Some(self.secret.clone()),
+            release_lock: Some(true),
+        };
+
+        self.client
+            .sdn_apply(params)
+            .await
+            .map(|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<Box<dyn PveClient + Send + Sync>, proxmox_client::Error> {
+        let params = ReleaseSdnLock {
+            force: force.into(),
+            lock_secret: Some(self.secret),
+        };
+
+        self.client.release_sdn_lock(params).await?;
+        Ok(self.client)
+    }
+}
+
+/// Releases all clients found in the [`clients`] parameter.
+///
+/// Any errors occuring during this process will get loggged, but the function will still try to
+/// release all other clients before returning.
+async fn release_clients(clients: HashMap<String, LockedSdnClient>) {
+    for (remote, client) in clients {
+        proxmox_log::info!("releasing lock for remote {remote}");
+
+        if let Err(error) = client.release(false).await {
+            proxmox_log::error!("could not release lock for remote {remote}: {error:#}",)
+        }
+    }
+}
+
+/// A convenience function for creating locked clients for multiple 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
+/// * any of the supplied remotes cannot be successfully locked
+///
+/// In any of those cases all remotes that have already been locked will get unlocked before the
+/// error gets returned.
+pub(crate) async fn create_locked_clients(
+    remotes: impl Iterator<Item = String>,
+) -> Result<HashMap<String, LockedSdnClient>, anyhow::Error> {
+    let (remote_config, _) = pdm_config::remotes::config()?;
+    let mut locked_clients = HashMap::new();
+
+    for remote in remotes {
+        proxmox_log::info!("obtaining lock for remote {remote}");
+
+        let Ok(remote_config) = get_remote(&remote_config, &remote) else {
+            release_clients(locked_clients).await;
+            bail!("remote {remote} does not exist in configuration");
+        };
+
+        let Ok(client) = LockedSdnClient::new(remote_config, false).await else {
+            release_clients(locked_clients).await;
+            bail!("could not lock sdn configuration for remote {remote}",);
+        };
+
+        locked_clients.insert(remote, client);
+    }
+
+    Ok(locked_clients)
+}
+
+// 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: Box<dyn PveClient + Send + Sync>,
+) -> Result<(RemoteUpid, Box<dyn PveClient + Send + Sync>), anyhow::Error> {
+    loop {
+        tokio::time::sleep(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, client));
+            } else {
+                bail!(
+                    "task did not finish successfully on remote {}",
+                    upid.remote()
+                );
+            }
+        }
+    }
+}
+
+/// Applies the SDN configuration for multiple locked clients.
+///
+/// This function tries to apply the SDN configuration for all supplied locked clients. It logs
+/// success and error messages via proxmox_log.
+///
+/// # Errors
+/// This function returns an error if applying the configuration on one of the remotes failed. It
+/// will always wait for all futures to finish and only return an error afterwards.
+pub(crate) async fn apply_sdn_configuration(
+    locked_clients: HashMap<String, LockedSdnClient>,
+) -> Result<(), anyhow::Error> {
+    let mut futures = FuturesUnordered::new();
+
+    for (id, client) in locked_clients.into_iter() {
+        proxmox_log::info!("applying sdn config on remote {id}");
+
+        let remote_id = id.clone();
+
+        let future = client
+            .apply_and_release()
+            .map_err(anyhow::Error::msg)
+            .and_then(move |(upid, client)| {
+                proxmox_log::info!("reloading SDN configuration on remote {}", remote_id);
+
+                let remote_upid =
+                    RemoteUpid::try_from((remote_id, upid.to_string())).expect("valid upid");
+
+                poll_task(upid.node.clone(), remote_upid, client)
+            });
+
+        futures.push(future);
+    }
+
+    proxmox_log::info!("Waiting for reload tasks to finish on all remotes, this can take awhile");
+
+    let mut errors = false;
+    while let Some(result) = futures.next().await {
+        match result {
+            Ok((upid, _)) => {
+                proxmox_log::info!(
+                    "successfully applied configuration on remote {}",
+                    upid.remote()
+                );
+            }
+            Err(error) => {
+                proxmox_log::error!("{error:#}",);
+                errors = true;
+            }
+        }
+    }
+
+    if errors {
+        bail!("failed to apply configuration on at least one remote");
+    }
+
+    Ok(())
+}
-- 
2.39.5


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


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

* [pdm-devel] [PATCH proxmox-datacenter-manager 02/13] api: sdn: add list_zones endpoint
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (13 preceding siblings ...)
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 01/13] server: add locked sdn client and helper methods Stefan Hanreich
@ 2025-02-28 15:17 ` Stefan Hanreich
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 03/13] api: sdn: add create_zone endpoint Stefan Hanreich
                   ` (10 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:17 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.

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    | 19 +++++++++
 server/src/api/mod.rs        |  2 +
 server/src/api/sdn/mod.rs    | 13 ++++++
 server/src/api/sdn/zones.rs  | 78 ++++++++++++++++++++++++++++++++++++
 7 files changed, 140 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 6575c03..04c226a 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 3844907..3566785 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 a41b82c..355d3ac 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);
@@ -842,6 +845,22 @@ impl<T: HttpApiClient> PdmClient<T> {
         add_query_arg(&mut path, &mut sep, "target", &target);
         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 mut path = "/api2/extjs/sdn/zones".to_string();
+
+        let mut sep = '?';
+        add_query_arg(&mut path, &mut sep, "pending", &pending.into());
+        add_query_arg(&mut path, &mut sep, "running", &running.into());
+        add_query_arg(&mut path, &mut sep, "ty", &ty.into());
+
+        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.39.5


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


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

* [pdm-devel] [PATCH proxmox-datacenter-manager 03/13] api: sdn: add create_zone endpoint
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (14 preceding siblings ...)
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 02/13] api: sdn: add list_zones endpoint Stefan Hanreich
@ 2025-02-28 15:17 ` Stefan Hanreich
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 04/13] api: sdn: add list_vnets endpoint Stefan Hanreich
                   ` (9 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:17 UTC (permalink / raw)
  To: pdm-devel

This endpoint is used for creating a new EVPN zone on multiple
remotes. It does the following actions when creating a new EVPN zone:

* lock the sdn configuration of all involved remotes
  * if it fails, release the lock on all remotes and return
    unsuccessfully
* invoke the API endpoints to create the new EVPN zone
  * if it fails, no further changes will be made to the remote and the
    configuration will stay locked, no rollbacks
* apply the configuration on all remotes where the changes were
  successful
  * any errors during applying the configuration on a remote will be
    logged
  * the configuration will stay locked if applying the configuration
    fails
* reload the network configuration on all nodes where applying was
  successful

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  | 111 +++++++++++++++++++++++++++++++++--
 3 files changed, 173 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 355d3ac..bd6ca63 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;
 }
 
@@ -861,6 +862,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..abdc737 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::{apply_sdn_configuration, create_locked_clients},
+};
 
-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,92 @@ 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 {
+            let mut locked_clients =
+                create_locked_clients(remotes.iter().map(|remote| remote.remote.clone())).await?;
+
+            for CreateZoneRemote { remote, controller } in remotes {
+                let create_zone = CreateZone {
+                    zone: zone.clone(),
+                    vrf_vxlan,
+                    controller: Some(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_secret: None,
+                };
+
+                let client = locked_clients
+                    .get(&remote)
+                    .expect("client has been created for remote");
+
+                proxmox_log::info!("creating zone {zone} on remote {remote}");
+
+                if let Err(error) = client.create_zone(create_zone).await {
+                    proxmox_log::error!("could not create vnet for remote {remote}: {error:#}",);
+                    locked_clients.remove(&remote);
+                }
+            }
+
+            apply_sdn_configuration(locked_clients).await
+        },
+    )?;
+
+    Ok(upid)
+}
-- 
2.39.5


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


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

* [pdm-devel] [PATCH proxmox-datacenter-manager 04/13] api: sdn: add list_vnets endpoint
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (15 preceding siblings ...)
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 03/13] api: sdn: add create_zone endpoint Stefan Hanreich
@ 2025-02-28 15:17 ` Stefan Hanreich
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 05/13] api: sdn: add create_vnet endpoint Stefan Hanreich
                   ` (8 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:17 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.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 lib/pdm-api-types/src/sdn.rs | 21 ++++++++++-
 lib/pdm-client/src/lib.rs    | 16 +++++++-
 server/src/api/sdn/mod.rs    |  2 +
 server/src/api/sdn/vnets.rs  | 72 ++++++++++++++++++++++++++++++++++++
 4 files changed, 109 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 bd6ca63..42ea7b1 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};
@@ -863,6 +863,20 @@ 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 mut path = "/api2/extjs/sdn/vnets".to_string();
+
+        let mut sep = '?';
+        add_query_arg(&mut path, &mut sep, "pending", &pending.into());
+        add_query_arg(&mut path, &mut sep, "running", &running.into());
+
+        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.39.5


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


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

* [pdm-devel] [PATCH proxmox-datacenter-manager 05/13] api: sdn: add create_vnet endpoint
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (16 preceding siblings ...)
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 04/13] api: sdn: add list_vnets endpoint Stefan Hanreich
@ 2025-02-28 15:17 ` Stefan Hanreich
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 06/13] api: sdn: add list_controllers endpoint Stefan Hanreich
                   ` (7 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:17 UTC (permalink / raw)
  To: pdm-devel

This endpoint is used for creating a new EVPN VNet on multiple
remotes. It does the following actions when creating a new EVPN VNet:

* lock the sdn configuration of all involved remotes
  * if it fails, release the lock on all remotes and return
    unsuccessfully
* invoke the API endpoints to create the new EVPN VNet
  * if it fails, no further changes will be made to the remote and the
    configuration will stay locked, no rollbacks
* apply the configuration on all remotes where the changes were
  successful
  * any errors during applying the configuration on a remote will be
    logged
  * the configuration will stay locked if applying the configuration
    fails
* reload the network configuration on all nodes where applying was
  successful

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  | 89 ++++++++++++++++++++++++++++++++++--
 3 files changed, 136 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 42ea7b1..7ed6c62 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;
 }
 
@@ -882,6 +882,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..58a1ba4 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::{apply_sdn_configuration, create_locked_clients},
+};
 
-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,71 @@ 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 {
+            let mut locked_clients =
+                create_locked_clients(remotes.iter().map(|remote| remote.remote.clone())).await?;
+
+            for CreateVnetRemote { remote, zone } in &remotes {
+                let create_vnet = CreateVnet {
+                    alias: None,
+                    isolate_ports: None,
+                    tag,
+                    ty: Some(SdnVnetType::Vnet),
+                    vlanaware: None,
+                    vnet: vnet.to_string(),
+                    zone: zone.clone(),
+                    lock_secret: None,
+                };
+
+                let client = locked_clients
+                    .get(remote)
+                    .expect("client has been created for remote");
+
+                proxmox_log::info!("creating vnet {vnet} on remote {remote}");
+
+                if let Err(error) = client.create_vnet(create_vnet).await {
+                    proxmox_log::error!("could not create vnet for remote {remote}: {error:#}",);
+                    locked_clients.remove(remote);
+                }
+            }
+
+            apply_sdn_configuration(locked_clients).await
+        },
+    )?;
+
+    Ok(upid)
+}
-- 
2.39.5


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


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

* [pdm-devel] [PATCH proxmox-datacenter-manager 06/13] api: sdn: add list_controllers endpoint
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (17 preceding siblings ...)
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 05/13] api: sdn: add create_vnet endpoint Stefan Hanreich
@ 2025-02-28 15:17 ` Stefan Hanreich
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 07/13] ui: add VrfTree component Stefan Hanreich
                   ` (6 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:17 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         | 22 ++++++++-
 server/src/api/sdn/controllers.rs | 78 +++++++++++++++++++++++++++++++
 server/src/api/sdn/mod.rs         |  2 +
 4 files changed, 120 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 7ed6c62..94d1f87 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);
@@ -847,6 +849,22 @@ 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 mut path = "/api2/extjs/sdn/controllers".to_string();
+
+        let mut sep = '?';
+        add_query_arg(&mut path, &mut sep, "pending", &pending.into());
+        add_query_arg(&mut path, &mut sep, "running", &running.into());
+        add_query_arg(&mut path, &mut sep, "ty", &ty.into());
+
+        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.39.5


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


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

* [pdm-devel] [PATCH proxmox-datacenter-manager 07/13] ui: add VrfTree component
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (18 preceding siblings ...)
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 06/13] api: sdn: add list_controllers endpoint Stefan Hanreich
@ 2025-02-28 15:17 ` Stefan Hanreich
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 08/13] ui: sdn: add RouterTable component Stefan Hanreich
                   ` (5 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:17 UTC (permalink / raw)
  To: pdm-devel

This component shows a tree of all EVPN Zones of all remotes
configured in the PDM. They are grouped by VRF (= VRF VXLAN VNI of the
EVPN zone) first. The second level are the remotes, so users can see
all remotes where the VRF already exists. The third level, below the
respective remote, shows all VNets in the zones as well as their VXLAN
VNI.

I've decided to refer to EVPN Zones in the PDM UI as VRF, since this
is the well-established name for a routing table in this context. It
makes the purpose of having multiple zones clearer, since users were
confused about how to create multiple VRFs. By changing the name in
the PDM UI, this should be a lot easier to grasp for users not
familiar with the SDN stack. It also harmonizes our terminology with
the well-established standard terminology used in RFC 8356 [1]

[1] 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      |   2 +
 ui/src/sdn/evpn/vrf_tree.rs | 291 ++++++++++++++++++++++++++++++++++++
 ui/src/sdn/mod.rs           |   1 +
 4 files changed, 296 insertions(+)
 create mode 100644 ui/src/sdn/evpn/mod.rs
 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
new file mode 100644
index 0000000..0745f52
--- /dev/null
+++ b/ui/src/sdn/evpn/mod.rs
@@ -0,0 +1,2 @@
+mod vrf_tree;
+pub use vrf_tree::VrfTree;
diff --git a/ui/src/sdn/evpn/vrf_tree.rs b/ui/src/sdn/evpn/vrf_tree.rs
new file mode 100644
index 0000000..5e8005c
--- /dev/null
+++ b/ui/src/sdn/evpn/vrf_tree.rs
@@ -0,0 +1,291 @@
+use std::cmp::Ordering;
+use std::collections::HashMap;
+use std::rc::Rc;
+
+use pdm_client::types::{ListController, ListVnet, ListZone, Remote, SdnObjectState};
+use pwt::css;
+use pwt::props::{ContainerBuilder, EventSubscriber, ExtractPrimaryKey, WidgetBuilder};
+use pwt::state::{Selection, SlabTree, TreeStore};
+use pwt::tr;
+use pwt::widget::data_table::{
+    DataTable, DataTableCellRenderArgs, DataTableColumn, DataTableHeader, DataTableRowRenderArgs,
+};
+use pwt::widget::{Button, Column, Container, Fa, Row, Toolbar};
+use pwt_macros::widget;
+use yew::virtual_dom::Key;
+use yew::{html, Callback, Component, Context, Html, MouseEvent, Properties};
+
+#[widget(comp=VrfTreeComponent)]
+#[derive(Clone, PartialEq, Properties)]
+pub struct VrfTree {
+    zones: Rc<Vec<ListZone>>,
+    vnets: Rc<Vec<ListVnet>>,
+    remotes: Rc<Vec<Remote>>,
+    controllers: Rc<Vec<ListController>>,
+    on_add: Callback<MouseEvent>,
+    on_add_vnet: Callback<MouseEvent>,
+}
+
+impl VrfTree {
+    pub fn new(
+        zones: Rc<Vec<ListZone>>,
+        vnets: Rc<Vec<ListVnet>>,
+        remotes: Rc<Vec<Remote>>,
+        controllers: Rc<Vec<ListController>>,
+        on_add: Callback<MouseEvent>,
+        on_add_vnet: Callback<MouseEvent>,
+    ) -> Self {
+        yew::props!(Self {
+            zones,
+            vnets,
+            remotes,
+            controllers,
+            on_add,
+            on_add_vnet,
+        })
+    }
+}
+
+pub enum VrfTreeMsg {
+    SelectionChange,
+}
+
+#[derive(Clone, PartialEq, Debug)]
+pub enum VrfTreeEntry {
+    Root(Key),
+    Vrf {
+        vni: u32,
+    },
+    Remote {
+        id: String,
+        zone: String,
+        state: Option<SdnObjectState>,
+    },
+    Vnet {
+        id: String,
+        remote: String,
+        vxlan_id: u32,
+        state: Option<SdnObjectState>,
+    },
+}
+
+impl ExtractPrimaryKey for VrfTreeEntry {
+    fn extract_key(&self) -> Key {
+        match self {
+            Self::Root(key) => key.clone(),
+            Self::Vrf { vni } => Key::from(vni.to_string()),
+            Self::Remote { id, zone, .. } => Key::from(format!("{}/{}", id, zone)),
+            Self::Vnet {
+                id,
+                remote,
+                vxlan_id,
+                ..
+            } => Key::from(format!("{}/{}/{}", id, remote, vxlan_id)),
+        }
+    }
+}
+
+fn zones_to_vrf_view(zones: &[ListZone], vnets: &[ListVnet]) -> SlabTree<VrfTreeEntry> {
+    let mut tree = SlabTree::new();
+
+    let mut vrfs = HashMap::new();
+    let mut zone_vnets = HashMap::new();
+
+    for zone in zones {
+        vrfs.entry(zone.zone.vrf_vxlan.expect("EVPN zone has a VXLAN ID"))
+            .or_insert(Vec::new())
+            .push(zone);
+    }
+
+    for vnet in vnets {
+        zone_vnets
+            .entry(format!("{}/{}", vnet.remote, vnet.vnet.zone_pending()))
+            .or_insert(Vec::new())
+            .push(vnet);
+    }
+
+    let mut root = tree.set_root(VrfTreeEntry::Root(Key::from("root")));
+    root.set_expanded(true);
+
+    for (vni, zones) in vrfs {
+        let mut vrf_entry = root.append(VrfTreeEntry::Vrf { vni });
+        vrf_entry.set_expanded(true);
+
+        for zone in zones {
+            let mut remote_entry = vrf_entry.append(VrfTreeEntry::Remote {
+                id: zone.remote.clone(),
+                zone: zone.zone.zone.clone(),
+                state: zone.zone.state,
+            });
+
+            remote_entry.set_expanded(true);
+
+            let key = format!("{}/{}", zone.remote, zone.zone.zone);
+            if let Some(vnets) = zone_vnets.get(&key) {
+                for vnet in vnets {
+                    remote_entry.append(VrfTreeEntry::Vnet {
+                        id: vnet.vnet.vnet.clone(),
+                        remote: vnet.remote.clone(),
+                        vxlan_id: vnet.vnet.tag_pending().expect("EVPN VNet has a tag."),
+                        state: vnet.vnet.state,
+                    });
+                }
+            }
+        }
+    }
+
+    tree
+}
+
+fn render_vxlan(args: &mut DataTableCellRenderArgs<VrfTreeEntry>) -> Html {
+    match args.record() {
+        VrfTreeEntry::Vnet { vxlan_id, .. } => vxlan_id.to_string().as_str().into(),
+        _ => html! {},
+    }
+}
+
+fn render_zone(args: &mut DataTableCellRenderArgs<VrfTreeEntry>) -> Html {
+    match args.record() {
+        VrfTreeEntry::Remote { zone, .. } => zone.as_str().into(),
+        _ => html! {},
+    }
+}
+
+fn render_remote_or_vrf(args: &mut DataTableCellRenderArgs<VrfTreeEntry>) -> Html {
+    let mut row = Row::new().class(css::AlignItems::Baseline).gap(2);
+
+    row = match args.record() {
+        VrfTreeEntry::Vrf { vni } => row
+            .with_child(Fa::new("th"))
+            .with_child(format!("VRF {vni}")),
+        VrfTreeEntry::Remote { id, .. } => {
+            row.with_child(Fa::new("server")).with_child(id.as_str())
+        }
+        VrfTreeEntry::Vnet { id, .. } => row
+            .with_child(Fa::new("network-wired"))
+            .with_child(id.as_str()),
+        _ => row,
+    };
+
+    row.into()
+}
+
+fn render_state(args: &mut DataTableCellRenderArgs<VrfTreeEntry>) -> Html {
+    let state = match args.record() {
+        VrfTreeEntry::Remote { state, .. } => *state,
+        VrfTreeEntry::Vnet { state, .. } => *state,
+        _ => None,
+    };
+
+    match state {
+        Some(SdnObjectState::New) => "new",
+        Some(SdnObjectState::Changed) => "changed",
+        Some(SdnObjectState::Deleted) => "deleted",
+        None => "",
+    }
+    .into()
+}
+
+fn vrf_sorter(a: &VrfTreeEntry, b: &VrfTreeEntry) -> Ordering {
+    match (a, b) {
+        (VrfTreeEntry::Vrf { vni: vni_a }, VrfTreeEntry::Vrf { vni: vni_b }) => vni_a.cmp(vni_b),
+        (VrfTreeEntry::Remote { id: id_a, .. }, VrfTreeEntry::Remote { id: id_b, .. }) => {
+            id_a.cmp(id_b)
+        }
+        (
+            VrfTreeEntry::Vnet {
+                vxlan_id: vxlan_id_a,
+                ..
+            },
+            VrfTreeEntry::Vnet {
+                vxlan_id: vxlan_id_b,
+                ..
+            },
+        ) => vxlan_id_a.cmp(vxlan_id_b),
+        (_, _) => std::cmp::Ordering::Equal,
+    }
+}
+
+pub struct VrfTreeComponent {
+    store: TreeStore<VrfTreeEntry>,
+    selection: Selection,
+}
+
+impl VrfTreeComponent {
+    fn columns(store: TreeStore<VrfTreeEntry>) -> Rc<Vec<DataTableHeader<VrfTreeEntry>>> {
+        Rc::new(vec![
+            DataTableColumn::new(tr!("VRF / Remote"))
+                .tree_column(store)
+                .render_cell(render_remote_or_vrf)
+                .sorter(vrf_sorter)
+                .into(),
+            DataTableColumn::new(tr!("Zone"))
+                .render_cell(render_zone)
+                .into(),
+            DataTableColumn::new(tr!("VXLAN"))
+                .render_cell(render_vxlan)
+                .into(),
+            DataTableColumn::new(tr!("State"))
+                .render_cell(render_state)
+                .into(),
+        ])
+    }
+}
+
+impl Component for VrfTreeComponent {
+    type Properties = VrfTree;
+    type Message = VrfTreeMsg;
+
+    fn create(ctx: &Context<Self>) -> Self {
+        let store = TreeStore::new().view_root(false);
+        store.set_sorter(vrf_sorter);
+
+        let selection =
+            Selection::new().on_select(ctx.link().callback(|_| Self::Message::SelectionChange));
+
+        Self { store, selection }
+    }
+
+    fn view(&self, ctx: &Context<Self>) -> Html {
+        let toolbar = Toolbar::new()
+            .class("pwt-w-100")
+            .class("pwt-overflow-hidden")
+            .class("pwt-border-bottom")
+            .with_child(Button::new(tr!("Add")).onclick(ctx.props().on_add.clone()))
+            .with_child(Button::new(tr!("Add VNet")).onclick(ctx.props().on_add_vnet.clone()));
+
+        let columns = Self::columns(self.store.clone());
+
+        let table = DataTable::new(columns, self.store.clone())
+            .selection(self.selection.clone())
+            .striped(false)
+            .row_render_callback(|args: &mut DataTableRowRenderArgs<VrfTreeEntry>| {
+                if let VrfTreeEntry::Vrf { .. } = args.record() {
+                    args.add_class("pwt-bg-color-surface");
+                }
+            })
+            .class(css::FlexFit);
+
+        Container::new()
+            .class(css::FlexFit)
+            .with_child(
+                Column::new()
+                    .class(css::FlexFit)
+                    .with_child(toolbar)
+                    .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) {
+            let data = zones_to_vrf_view(&ctx.props().zones, &ctx.props().vnets);
+            self.store.set_data(data);
+            self.store.set_sorter(vrf_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.39.5


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


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

* [pdm-devel] [PATCH proxmox-datacenter-manager 08/13] ui: sdn: add RouterTable component
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (19 preceding siblings ...)
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 07/13] ui: add VrfTree component Stefan Hanreich
@ 2025-02-28 15:17 ` Stefan Hanreich
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 09/13] ui: sdn: add AddVnetWindow component Stefan Hanreich
                   ` (4 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:17 UTC (permalink / raw)
  To: pdm-devel

This table shows an overview of EVPN controllers on all connected
remotes. Those routers can then be used for creating new EVPN zones.

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

diff --git a/ui/src/sdn/evpn/mod.rs b/ui/src/sdn/evpn/mod.rs
index 0745f52..996ab25 100644
--- a/ui/src/sdn/evpn/mod.rs
+++ b/ui/src/sdn/evpn/mod.rs
@@ -1,2 +1,5 @@
+mod router_table;
+pub use router_table::RouterTable;
+
 mod vrf_tree;
 pub use vrf_tree::VrfTree;
diff --git a/ui/src/sdn/evpn/router_table.rs b/ui/src/sdn/evpn/router_table.rs
new file mode 100644
index 0000000..8f2892c
--- /dev/null
+++ b/ui/src/sdn/evpn/router_table.rs
@@ -0,0 +1,125 @@
+use std::cmp::Ordering;
+use std::rc::Rc;
+
+use pdm_client::types::{ListController, SdnObjectState};
+use pwt::props::{ContainerBuilder, ExtractPrimaryKey, WidgetBuilder};
+use pwt::state::{Selection, Store};
+use pwt::widget::data_table::{DataTable, DataTableColumn, DataTableHeader};
+use pwt::widget::{Column, Container};
+use pwt::{css, tr};
+use serde::{Deserialize, Serialize};
+use yew::virtual_dom::{Key, VComp, VNode};
+use yew::{Component, Context, Html, Properties};
+
+#[derive(PartialEq, Properties)]
+pub struct RouterTable {
+    controllers: Rc<Vec<ListController>>,
+}
+
+impl RouterTable {
+    pub fn new(controllers: Rc<Vec<ListController>>) -> Self {
+        yew::props!(Self { controllers })
+    }
+}
+
+impl From<RouterTable> for VNode {
+    fn from(val: RouterTable) -> Self {
+        let comp = VComp::new::<RouterTableComponent>(Rc::new(val), None);
+        VNode::from(comp)
+    }
+}
+
+pub enum RouterTableMsg {
+    SelectionChange,
+}
+
+#[derive(Clone, PartialEq, Serialize, Deserialize)]
+pub struct RouterEntry {
+    pub remote: String,
+    pub controller: String,
+    pub asn: u32,
+    pub state: Option<SdnObjectState>,
+}
+
+impl ExtractPrimaryKey for RouterEntry {
+    fn extract_key(&self) -> Key {
+        Key::from(format!("{}/{}", self.remote, self.controller))
+    }
+}
+
+pub struct RouterTableComponent {
+    store: Store<RouterEntry>,
+    selection: Selection,
+}
+
+fn remote_sorter(a: &RouterEntry, b: &RouterEntry) -> Ordering {
+    a.remote.cmp(&b.remote)
+}
+
+impl RouterTableComponent {
+    fn columns() -> Rc<Vec<DataTableHeader<RouterEntry>>> {
+        Rc::new(vec![
+            DataTableColumn::new(tr!("Remote"))
+                .render(|item: &RouterEntry| item.remote.as_str().into())
+                .sorter(remote_sorter)
+                .into(),
+            DataTableColumn::new(tr!("Name"))
+                .render(|item: &RouterEntry| item.controller.as_str().into())
+                .sorter(|a: &RouterEntry, b: &RouterEntry| a.controller.cmp(&b.controller))
+                .into(),
+            DataTableColumn::new(tr!("ASN"))
+                .render(|item: &RouterEntry| item.asn.into())
+                .sorter(|a: &RouterEntry, b: &RouterEntry| a.asn.cmp(&b.asn))
+                .into(),
+        ])
+    }
+}
+
+impl Component for RouterTableComponent {
+    type Properties = RouterTable;
+    type Message = RouterTableMsg;
+
+    fn create(ctx: &Context<Self>) -> Self {
+        let store = Store::new();
+        store.set_sorter(remote_sorter);
+
+        let selection =
+            Selection::new().on_select(ctx.link().callback(|_| Self::Message::SelectionChange));
+
+        Self { store, selection }
+    }
+
+    fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
+        if !Rc::ptr_eq(&ctx.props().controllers, &old_props.controllers) {
+            self.store.set_data(
+                ctx.props()
+                    .controllers
+                    .iter()
+                    .map(|elem| RouterEntry {
+                        remote: elem.remote.clone(),
+                        controller: elem.controller.controller.clone(),
+                        asn: elem
+                            .controller
+                            .asn_pending()
+                            .expect("EVPN controller has an ASN"),
+                        state: elem.controller.state,
+                    })
+                    .collect(),
+            );
+            self.store.set_sorter(remote_sorter);
+        }
+
+        true
+    }
+
+    fn view(&self, _ctx: &Context<Self>) -> Html {
+        let table = DataTable::new(Self::columns(), self.store.clone())
+            .selection(self.selection.clone())
+            .class("pwt-flex-fit");
+
+        Container::new()
+            .class(css::FlexFit)
+            .with_child(Column::new().class(css::FlexFit).gap(2).with_child(table))
+            .into()
+    }
+}
-- 
2.39.5


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


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

* [pdm-devel] [PATCH proxmox-datacenter-manager 09/13] ui: sdn: add AddVnetWindow component
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (20 preceding siblings ...)
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 08/13] ui: sdn: add RouterTable component Stefan Hanreich
@ 2025-02-28 15:17 ` Stefan Hanreich
  2025-02-28 15:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager 10/13] ui: sdn: add AddZoneWindow component Stefan Hanreich
                   ` (3 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:17 UTC (permalink / raw)
  To: pdm-devel

Adds an edit window for creating a new VNet. 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 | 216 ++++++++++++++++++++++++++++++++++++
 ui/src/sdn/evpn/mod.rs      |   3 +
 3 files changed, 220 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 94d1f87..049d5a3 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..05ecfbc
--- /dev/null
+++ b/ui/src/sdn/evpn/add_vnet.rs
@@ -0,0 +1,216 @@
+use std::{collections::HashSet, rc::Rc};
+
+use anyhow::{bail, Error};
+use pdm_client::types::{ListZone, SDN_ID_SCHEMA};
+use proxmox_yew_comp::{EditWindow, SchemaValidation};
+use pwt::{
+    css,
+    props::{
+        ContainerBuilder, CssBorderBuilder, CssPaddingBuilder, ExtractPrimaryKey, FieldBuilder,
+        SubmitCallback, 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 serde::{Deserialize, Serialize};
+use serde_json::Value;
+use yew::{function_component, virtual_dom::Key, Callback, Html, Properties};
+
+#[derive(Properties, PartialEq)]
+pub struct AddVnetWindowProps {
+    pub zones: Rc<Vec<ListZone>>,
+    pub on_submit_callback: SubmitCallback<FormContext>,
+    pub on_close_callback: Callback<()>,
+}
+
+#[function_component]
+pub fn AddVnetWindow(props: &AddVnetWindowProps) -> Html {
+    let zones = props.zones.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(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(props.on_close_callback.clone())
+        .on_submit(Some(props.on_submit_callback.clone()))
+        .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,
+}
+
+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(),
+                })
+                .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(),
+        ]);
+}
diff --git a/ui/src/sdn/evpn/mod.rs b/ui/src/sdn/evpn/mod.rs
index 996ab25..4db42b4 100644
--- a/ui/src/sdn/evpn/mod.rs
+++ b/ui/src/sdn/evpn/mod.rs
@@ -3,3 +3,6 @@ pub use router_table::RouterTable;
 
 mod vrf_tree;
 pub use vrf_tree::VrfTree;
+
+mod add_vnet;
+pub use add_vnet::AddVnetWindow;
-- 
2.39.5


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


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

* [pdm-devel] [PATCH proxmox-datacenter-manager 10/13] ui: sdn: add AddZoneWindow component
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (21 preceding siblings ...)
  2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 09/13] ui: sdn: add AddVnetWindow component Stefan Hanreich
@ 2025-02-28 15:18 ` Stefan Hanreich
  2025-02-28 15:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager 11/13] ui: sdn: add EvpnPanel Stefan Hanreich
                   ` (2 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:18 UTC (permalink / raw)
  To: pdm-devel

Adds an edit window for creating a new zone. This windows shows a form
containing all fields required to create new zone via the create_zone
API endpoint.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 ui/src/sdn/evpn/add_zone.rs | 229 ++++++++++++++++++++++++++++++++++++
 ui/src/sdn/evpn/mod.rs      |   3 +
 2 files changed, 232 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..3b6b0cb
--- /dev/null
+++ b/ui/src/sdn/evpn/add_zone.rs
@@ -0,0 +1,229 @@
+use std::{collections::HashSet, rc::Rc};
+
+use anyhow::{bail, Error};
+use pdm_client::types::{ListController, SDN_ID_SCHEMA};
+use proxmox_yew_comp::{EditWindow, SchemaValidation};
+use pwt::{
+    css,
+    props::{
+        ContainerBuilder, CssBorderBuilder, CssPaddingBuilder, ExtractPrimaryKey, FieldBuilder,
+        SubmitCallback, 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 serde::{Deserialize, Serialize};
+use serde_json::Value;
+use yew::{function_component, virtual_dom::Key, Callback, Html, Properties};
+
+#[derive(Properties, PartialEq)]
+pub struct AddZoneWindowProps {
+    pub controllers: Rc<Vec<ListController>>,
+    pub on_submit_callback: SubmitCallback<FormContext>,
+    pub on_close_callback: Callback<()>,
+}
+
+#[function_component]
+pub fn AddZoneWindow(props: &AddZoneWindowProps) -> Html {
+    let controllers = props.controllers.clone();
+
+    EditWindow::new(tr!("Add VRF"))
+        .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(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(props.on_close_callback.clone())
+        .on_submit(Some(props.on_submit_callback.clone()))
+        .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 4db42b4..f02eb7b 100644
--- a/ui/src/sdn/evpn/mod.rs
+++ b/ui/src/sdn/evpn/mod.rs
@@ -6,3 +6,6 @@ pub use vrf_tree::VrfTree;
 
 mod add_vnet;
 pub use add_vnet::AddVnetWindow;
+
+mod add_zone;
+pub use add_zone::AddZoneWindow;
-- 
2.39.5


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


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

* [pdm-devel] [PATCH proxmox-datacenter-manager 11/13] ui: sdn: add EvpnPanel
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (22 preceding siblings ...)
  2025-02-28 15:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager 10/13] ui: sdn: add AddZoneWindow component Stefan Hanreich
@ 2025-02-28 15:18 ` Stefan Hanreich
  2025-02-28 15:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager 12/13] ui: sdn: add EvpnPanel to main menu Stefan Hanreich
  2025-02-28 15:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager 13/13] pve: sdn: add descriptions for sdn tasks Stefan Hanreich
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:18 UTC (permalink / raw)
  To: pdm-devel

Add a panel that provides an overview of the current state of all EVPN
zones / vnets / controllers of all remotes configured in PDM.

It uses the RouterTable and VrfTree components for rendering the
current state of EVPN on all clusters. This assumes that all zones
with the same VRF VXLAN ID belong to the same VRF instance. This does
not work if an EVPN zone imports routes from VRFs with different VNIs.

In the future, for a more advanced overview, we should incorporate the
information about which route targets get imported from the EVPN
configuration and render the zones accordingly.

The panel stores all the information about the different entities and
passes them to the children as properties, in order to avoid making
the GET calls multiple times, since they're expensive. This can be
changed as soon as we introduce a caching mechanism for those API
endpoints.

When we do this, we can also change to using LoaderCallbacks instead
of just manually invoking the methods of the API client.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 ui/src/sdn/evpn/evpn_panel.rs | 249 ++++++++++++++++++++++++++++++++++
 ui/src/sdn/evpn/mod.rs        |   3 +
 2 files changed, 252 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..844ca59
--- /dev/null
+++ b/ui/src/sdn/evpn/evpn_panel.rs
@@ -0,0 +1,249 @@
+use std::rc::Rc;
+
+use anyhow::Error;
+use futures::try_join;
+use pdm_client::types::{
+    CreateVnetParams, CreateZoneParams, ListController, ListControllersType, ListVnet, ListZone,
+    ListZonesType, Remote,
+};
+use pwt::props::{ContainerBuilder, EventSubscriber, SubmitCallback, WidgetBuilder};
+use pwt::tr;
+use pwt::widget::form::FormContext;
+use pwt::widget::{Button, Column, Panel, Toolbar};
+use yew::virtual_dom::{VComp, VNode};
+use yew::{html, Html, Properties};
+
+use proxmox_yew_comp::{LoadableComponent, LoadableComponentContext, LoadableComponentMaster};
+
+use crate::pdm_client;
+use crate::sdn::evpn::{AddVnetWindow, AddZoneWindow, RouterTable, 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 {
+        remotes: Rc<Vec<Remote>>,
+        controllers: Rc<Vec<ListController>>,
+        zones: Rc<Vec<ListZone>>,
+        vnets: Rc<Vec<ListVnet>>,
+    },
+}
+
+#[derive(Debug, PartialEq)]
+pub enum EvpnPanelViewState {
+    AddVrf,
+    AddVnet,
+}
+
+async fn load_zones() -> Result<Vec<ListZone>, Error> {
+    let client = pdm_client();
+    let data = client
+        .pve_sdn_list_zones(true, false, ListZonesType::Evpn)
+        .await?;
+    Ok(data)
+}
+
+async fn load_controllers() -> Result<Vec<ListController>, Error> {
+    let client = pdm_client();
+    let data = client
+        .pve_sdn_list_controllers(true, false, ListControllersType::Evpn)
+        .await?;
+    Ok(data)
+}
+
+async fn load_remotes() -> Result<Vec<Remote>, Error> {
+    let client = pdm_client();
+    let data = client.list_remotes().await?;
+    Ok(data)
+}
+
+async fn load_vnets() -> Result<Vec<ListVnet>, Error> {
+    let client = pdm_client();
+    let data = client.pve_sdn_list_vnets(true, false).await?;
+    Ok(data)
+}
+
+pub struct EvpnPanelComponent {
+    remotes: Rc<Vec<Remote>>,
+    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 {
+            remotes: Default::default(),
+            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 (remotes, controllers, zones, vnets) = try_join!(
+                load_remotes(),
+                load_controllers(),
+                load_zones(),
+                load_vnets()
+            )?;
+
+            link.send_message(Self::Message::LoadFinished {
+                remotes: Rc::new(remotes),
+                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 {
+                remotes,
+                controllers,
+                zones,
+                vnets,
+            } => {
+                self.remotes = remotes;
+                self.controllers = controllers;
+                self.zones = zones;
+                self.vnets = vnets;
+            }
+            Self::Message::Reload => {
+                ctx.link().send_reload();
+            }
+        }
+
+        true
+    }
+
+    fn main_view(&self, ctx: &LoadableComponentContext<Self>) -> Html {
+        let on_add_vrf = ctx
+            .link()
+            .change_view_callback(|_| Some(Self::ViewState::AddVrf));
+
+        let on_add_vnet = 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(
+                Button::new(tr!("Refresh")).onclick(ctx.link().callback(|_| Self::Message::Reload)),
+            );
+
+        Column::new()
+            .class("pwt-flex-fit")
+            .with_child(toolbar)
+            .with_child(
+                Panel::new()
+                    .title(tr!("Router"))
+                    .with_child(RouterTable::new(self.controllers.clone())),
+            )
+            .with_child(
+                Panel::new()
+                    .title(tr!("VRF (Zone)"))
+                    .with_child(VrfTree::new(
+                        self.zones.clone(),
+                        self.vnets.clone(),
+                        self.remotes.clone(),
+                        self.controllers.clone(),
+                        on_add_vrf,
+                        on_add_vnet,
+                    )),
+            )
+            .into()
+    }
+
+    fn dialog_view(
+        &self,
+        ctx: &LoadableComponentContext<Self>,
+        view_state: &Self::ViewState,
+    ) -> Option<Html> {
+        let on_close_callback = ctx.link().clone().change_view_callback(|_| None);
+
+        Some(match view_state {
+            EvpnPanelViewState::AddVrf => {
+                let submit_scope = ctx.link().clone();
+
+                let on_submit_callback = SubmitCallback::new(move |form_ctx: FormContext| {
+                    let client = pdm_client();
+
+                    let params: CreateZoneParams =
+                        serde_json::from_value(form_ctx.get_submit_data()).unwrap();
+
+                    let scope = submit_scope.clone();
+                    async move {
+                        let upid = client.pve_sdn_create_zone(params).await?;
+                        scope.show_task_log(upid, None);
+                        Ok(())
+                    }
+                });
+
+                let controllers = self.controllers.clone();
+
+                html! {
+                    <AddZoneWindow {on_submit_callback} {on_close_callback} {controllers} />
+                }
+            }
+            EvpnPanelViewState::AddVnet => {
+                let submit_scope = ctx.link().clone();
+
+                let on_submit_callback = SubmitCallback::new(move |form_ctx: FormContext| {
+                    let client = pdm_client();
+                    let params: CreateVnetParams =
+                        serde_json::from_value(form_ctx.get_submit_data()).unwrap();
+
+                    let scope = submit_scope.clone();
+                    async move {
+                        let upid = client.pve_sdn_create_vnet(params).await?;
+                        scope.show_task_log(upid, None);
+                        Ok(())
+                    }
+                });
+
+                let zones = self.zones.clone();
+
+                html! {
+                    <AddVnetWindow {on_submit_callback} {on_close_callback} {zones} />
+                }
+            }
+        })
+    }
+}
diff --git a/ui/src/sdn/evpn/mod.rs b/ui/src/sdn/evpn/mod.rs
index f02eb7b..63f1959 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 router_table;
 pub use router_table::RouterTable;
 
-- 
2.39.5


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


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

* [pdm-devel] [PATCH proxmox-datacenter-manager 12/13] ui: sdn: add EvpnPanel to main menu
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (23 preceding siblings ...)
  2025-02-28 15:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager 11/13] ui: sdn: add EvpnPanel Stefan Hanreich
@ 2025-02-28 15:18 ` Stefan Hanreich
  2025-02-28 15:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager 13/13] pve: sdn: add descriptions for sdn tasks Stefan Hanreich
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:18 UTC (permalink / raw)
  To: pdm-devel

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.39.5


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


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

* [pdm-devel] [PATCH proxmox-datacenter-manager 13/13] pve: sdn: add descriptions for sdn tasks
  2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
                   ` (24 preceding siblings ...)
  2025-02-28 15:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager 12/13] ui: sdn: add EvpnPanel to main menu Stefan Hanreich
@ 2025-02-28 15:18 ` Stefan Hanreich
  25 siblings, 0 replies; 27+ messages in thread
From: Stefan Hanreich @ 2025-02-28 15:18 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.39.5


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


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

end of thread, other threads:[~2025-02-28 15:25 UTC | newest]

Thread overview: 27+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-02-28 15:17 [pdm-devel] [RFC proxmox{-api-types, -yew-comp, -datacenter-manager} 00/26] Add initial SDN / EVPN integration Stefan Hanreich
2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 01/12] sdn: add list/create zone endpoints Stefan Hanreich
2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 02/12] sdn: generate zones endpoints Stefan Hanreich
2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 03/12] sdn: add list/create vnet endpoints Stefan Hanreich
2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 04/12] sdn: generate " Stefan Hanreich
2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 05/12] sdn: add list/create controller endpoints Stefan Hanreich
2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 06/12] sdn: generate " Stefan Hanreich
2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 07/12] sdn: add acquire/release lock endpoints Stefan Hanreich
2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 08/12] sdn: generate " Stefan Hanreich
2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 09/12] sdn: add apply configuration endpoint Stefan Hanreich
2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 10/12] sdn: generate " Stefan Hanreich
2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 11/12] tasks: add helper for querying successfully finished tasks Stefan Hanreich
2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-api-types 12/12] sdn: add helpers for pending values Stefan Hanreich
2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-yew-comp 1/1] sdn: add descriptions for sdn tasks Stefan Hanreich
2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 01/13] server: add locked sdn client and helper methods Stefan Hanreich
2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 02/13] api: sdn: add list_zones endpoint Stefan Hanreich
2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 03/13] api: sdn: add create_zone endpoint Stefan Hanreich
2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 04/13] api: sdn: add list_vnets endpoint Stefan Hanreich
2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 05/13] api: sdn: add create_vnet endpoint Stefan Hanreich
2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 06/13] api: sdn: add list_controllers endpoint Stefan Hanreich
2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 07/13] ui: add VrfTree component Stefan Hanreich
2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 08/13] ui: sdn: add RouterTable component Stefan Hanreich
2025-02-28 15:17 ` [pdm-devel] [PATCH proxmox-datacenter-manager 09/13] ui: sdn: add AddVnetWindow component Stefan Hanreich
2025-02-28 15:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager 10/13] ui: sdn: add AddZoneWindow component Stefan Hanreich
2025-02-28 15:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager 11/13] ui: sdn: add EvpnPanel Stefan Hanreich
2025-02-28 15:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager 12/13] ui: sdn: add EvpnPanel to main menu Stefan Hanreich
2025-02-28 15:18 ` [pdm-devel] [PATCH proxmox-datacenter-manager 13/13] pve: sdn: add descriptions for sdn tasks Stefan Hanreich

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal