public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pdm-devel] [PATCH proxmox{, -datacenter-manager} 0/8] Integration of IP-VRF and MAC-VRF status to EVPN panel
@ 2025-11-07  8:59 Stefan Hanreich
  2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox 1/3] pve-api-types: add zone / vnet status reporting endpoints Stefan Hanreich
                   ` (7 more replies)
  0 siblings, 8 replies; 10+ messages in thread
From: Stefan Hanreich @ 2025-11-07  8:59 UTC (permalink / raw)
  To: pdm-devel

This patch series requires the API endpoints introduced in the Proxmox VE patch
series [1] - in particular the zone/ip-vrf and the vnet/mac-vrf endpoints.

It adds a second panel to the EVPN view and makes it a split view similar to the
remote view. It utilizes the endpoints to display status information about
selected zones / vnets.

[1] https://lore.proxmox.com/pve-devel/20251107085553.113655-1-s.hanreich@proxmox.com/T/

proxmox:

Stefan Hanreich (3):
  pve-api-types: add zone / vnet status reporting endpoints
  pve-api-types: generate ip-vrf / mac-vrf endpoints
  pve-api-types: regenerate

 pve-api-types/generate.pl            |   4 +
 pve-api-types/pve-api.json           | 286 ++++++++++++++++++++++++++-
 pve-api-types/src/generated/code.rs  |  25 +++
 pve-api-types/src/generated/types.rs | 104 ++++++++--
 4 files changed, 402 insertions(+), 17 deletions(-)


proxmox-datacenter-manager:

Stefan Hanreich (5):
  server: api: sdn: add ip-vrf endpoint
  server: api: sdn: add mac-vrf endpoint
  ui: sdn: evpn: add zone status panel
  ui: sdn: evpn: add vnet status panel
  sdn: evpn: add detail panel to the evpn panel

 lib/pdm-client/src/lib.rs      |  23 +++
 server/src/api/nodes/mod.rs    |   2 +
 server/src/api/nodes/sdn.rs    | 101 +++++++++++++
 server/src/api/pve/node.rs     |   3 +-
 ui/src/sdn/evpn/evpn_panel.rs  | 129 ++++++++++++++--
 ui/src/sdn/evpn/mod.rs         |  36 +++++
 ui/src/sdn/evpn/remote_tree.rs |  71 ++++++---
 ui/src/sdn/evpn/vnet_status.rs | 253 ++++++++++++++++++++++++++++++++
 ui/src/sdn/evpn/vrf_tree.rs    |  29 +++-
 ui/src/sdn/evpn/zone_status.rs | 261 +++++++++++++++++++++++++++++++++
 10 files changed, 874 insertions(+), 34 deletions(-)
 create mode 100644 server/src/api/nodes/sdn.rs
 create mode 100644 ui/src/sdn/evpn/vnet_status.rs
 create mode 100644 ui/src/sdn/evpn/zone_status.rs


Summary over all repositories:
  14 files changed, 1276 insertions(+), 51 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] 10+ messages in thread

* [pdm-devel] [PATCH proxmox 1/3] pve-api-types: add zone / vnet status reporting endpoints
  2025-11-07  8:59 [pdm-devel] [PATCH proxmox{, -datacenter-manager} 0/8] Integration of IP-VRF and MAC-VRF status to EVPN panel Stefan Hanreich
@ 2025-11-07  8:59 ` Stefan Hanreich
  2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox 2/3] pve-api-types: generate ip-vrf / mac-vrf endpoints Stefan Hanreich
                   ` (6 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Stefan Hanreich @ 2025-11-07  8:59 UTC (permalink / raw)
  To: pdm-devel

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

diff --git a/pve-api-types/pve-api.json b/pve-api-types/pve-api.json
index 1775fdae..7ac1244b 100644
--- a/pve-api-types/pve-api.json
+++ b/pve-api-types/pve-api.json
@@ -50740,12 +50740,173 @@
                                        "leaf": 1,
                                        "path": "/nodes/{node}/sdn/zones/{zone}/content",
                                        "text": "content"
+                                    },
+                                    {
+                                       "info": {
+                                          "GET": {
+                                             "allowtoken": 1,
+                                             "description": "Get a list of all bridges (vnets) that are part of a zone, as well as the ports that are members of that bridge.",
+                                             "method": "GET",
+                                             "name": "bridges",
+                                             "parameters": {
+                                                "additionalProperties": 0,
+                                                "properties": {
+                                                   "node": {
+                                                      "description": "The cluster node name.",
+                                                      "format": "pve-node",
+                                                      "type": "string"
+                                                   },
+                                                   "zone": {
+                                                      "description": "zone name or \"localnetwork\"",
+                                                      "type": "string"
+                                                   }
+                                                }
+                                             },
+                                             "permissions": {
+                                                "check": [
+                                                   "perm",
+                                                   "/sdn/zones/{zone}",
+                                                   [
+                                                      "SDN.Audit"
+                                                   ]
+                                                ]
+                                             },
+                                             "protected": 1,
+                                             "proxyto": "node",
+                                             "returns": {
+                                                "items": {
+                                                   "description": "List of bridges contained in the SDN zone.",
+                                                   "properties": {
+                                                      "name": {
+                                                         "description": "Name of the bridge.",
+                                                         "type": "string"
+                                                      },
+                                                      "ports": {
+                                                         "description": "All ports that are members of the bridge",
+                                                         "items": {
+                                                            "description": "Information about bridge ports.",
+                                                            "properties": {
+                                                               "index": {
+                                                                  "description": "The index of the guests network device that this interface belongs to.",
+                                                                  "optional": 1,
+                                                                  "type": "number"
+                                                               },
+                                                               "name": {
+                                                                  "description": "The name of the bridge port.",
+                                                                  "type": "string"
+                                                               },
+                                                               "primary_vlan": {
+                                                                  "description": "The primary VLAN configured for the port of this bridge (= PVID). Only for VLAN-aware bridges.",
+                                                                  "optional": 1,
+                                                                  "type": "number"
+                                                               },
+                                                               "vlans": {
+                                                                  "description": "A list of VLANs and VLAN ranges that are allowed for this bridge port in addition to the primary VLAN. Only for VLAN-aware bridges.",
+                                                                  "items": {
+                                                                     "description": "A single VLAN (123) or a VLAN range (234-435).",
+                                                                     "type": "string"
+                                                                  },
+                                                                  "optional": 1,
+                                                                  "type": "array"
+                                                               },
+                                                               "vmid": {
+                                                                  "description": "The ID of the guest that this interface belongs to.",
+                                                                  "optional": 1,
+                                                                  "type": "number"
+                                                               }
+                                                            },
+                                                            "type": "object"
+                                                         },
+                                                         "type": "array"
+                                                      },
+                                                      "vlan_filtering": {
+                                                         "description": "Whether VLAN filtering is enabled for this bridge (= VLAN-aware).",
+                                                         "type": "string"
+                                                      }
+                                                   },
+                                                   "type": "object"
+                                                },
+                                                "type": "array"
+                                             }
+                                          }
+                                       },
+                                       "leaf": 1,
+                                       "path": "/nodes/{node}/sdn/zones/{zone}/bridges",
+                                       "text": "bridges"
+                                    },
+                                    {
+                                       "info": {
+                                          "GET": {
+                                             "allowtoken": 1,
+                                             "description": "Get the IP VRF of an EVPN zone.",
+                                             "method": "GET",
+                                             "name": "ip-vrf",
+                                             "parameters": {
+                                                "additionalProperties": 0,
+                                                "properties": {
+                                                   "node": {
+                                                      "description": "The cluster node name.",
+                                                      "format": "pve-node",
+                                                      "type": "string"
+                                                   },
+                                                   "zone": {
+                                                      "description": "Name of an EVPN zone.",
+                                                      "type": "string"
+                                                   }
+                                                }
+                                             },
+                                             "permissions": {
+                                                "check": [
+                                                   "perm",
+                                                   "/sdn/zones/{zone}",
+                                                   [
+                                                      "SDN.Audit"
+                                                   ]
+                                                ]
+                                             },
+                                             "protected": 1,
+                                             "proxyto": "node",
+                                             "returns": {
+                                                "description": "All entries in the VRF table of zone {zone} of the node.This does not include /32 routes for guests on this host,since they are handled via the respective vnet bridge directly.",
+                                                "items": {
+                                                   "properties": {
+                                                      "ip": {
+                                                         "description": "The CIDR of the route table entry.",
+                                                         "format": "CIDR",
+                                                         "type": "string"
+                                                      },
+                                                      "metric": {
+                                                         "description": "This route's metric.",
+                                                         "type": "integer"
+                                                      },
+                                                      "nexthops": {
+                                                         "description": "A list of nexthops for the route table entry.",
+                                                         "items": {
+                                                            "description": "the interface name or ip address of the next hop",
+                                                            "type": "string"
+                                                         },
+                                                         "type": "array"
+                                                      },
+                                                      "protocol": {
+                                                         "description": "The protocol where this route was learned from (e.g. BGP).",
+                                                         "type": "string"
+                                                      }
+                                                   },
+                                                   "type": "object"
+                                                },
+                                                "type": "array"
+                                             }
+                                          }
+                                       },
+                                       "leaf": 1,
+                                       "path": "/nodes/{node}/sdn/zones/{zone}/ip-vrf",
+                                       "text": "ip-vrf"
                                     }
                                  ],
                                  "info": {
                                     "GET": {
                                        "allowtoken": 1,
-                                       "description": "",
+                                       "description": "Directory index for SDN zone status.",
                                        "method": "GET",
                                        "name": "diridx",
                                        "parameters": {
@@ -50769,9 +50930,7 @@
                                              "/sdn/zones/{zone}",
                                              [
                                                 "SDN.Audit"
-                                             ],
-                                             "any",
-                                             1
+                                             ]
                                           ]
                                        },
                                        "returns": {
@@ -50853,6 +51012,124 @@
                            "leaf": 0,
                            "path": "/nodes/{node}/sdn/zones",
                            "text": "zones"
+                        },
+                        {
+                           "children": [
+                              {
+                                 "children": [
+                                    {
+                                       "info": {
+                                          "GET": {
+                                             "allowtoken": 1,
+                                             "description": "Get the MAC VRF for a VNet in an EVPN zone.",
+                                             "method": "GET",
+                                             "name": "mac-vrf",
+                                             "parameters": {
+                                                "additionalProperties": 0,
+                                                "properties": {
+                                                   "node": {
+                                                      "description": "The cluster node name.",
+                                                      "format": "pve-node",
+                                                      "type": "string"
+                                                   },
+                                                   "vnet": {
+                                                      "completion": ("Code")[],
+                                                      "description": "The SDN vnet object identifier.",
+                                                      "format": "pve-sdn-vnet-id",
+                                                      "type": "string"
+                                                   }
+                                                }
+                                             },
+                                             "permissions": {
+                                                "description": "Require 'SDN.Audit' permissions on '/sdn/zones/<zone>/<vnet>'",
+                                                "user": "all"
+                                             },
+                                             "protected": 1,
+                                             "proxyto": "node",
+                                             "returns": {
+                                                "description": "All routes from the MAC VRF that this node self-originates or has learned via BGP.",
+                                                "items": {
+                                                   "properties": {
+                                                      "ip": {
+                                                         "description": "The IP address of the MAC VRF entry.",
+                                                         "format": "ip",
+                                                         "type": "string"
+                                                      },
+                                                      "mac": {
+                                                         "description": "The MAC address of the MAC VRF entry.",
+                                                         "format": "mac-addr",
+                                                         "type": "string"
+                                                      },
+                                                      "nexthop": {
+                                                         "description": "The IP address of the nexthop.",
+                                                         "format": "ip",
+                                                         "type": "string"
+                                                      }
+                                                   },
+                                                   "type": "object"
+                                                },
+                                                "type": "array"
+                                             }
+                                          }
+                                       },
+                                       "leaf": 1,
+                                       "path": "/nodes/{node}/sdn/vnets/{vnet}/mac-vrf",
+                                       "text": "mac-vrf"
+                                    }
+                                 ],
+                                 "info": {
+                                    "GET": {
+                                       "allowtoken": 1,
+                                       "description": "",
+                                       "method": "GET",
+                                       "name": "diridx",
+                                       "parameters": {
+                                          "additionalProperties": 0,
+                                          "properties": {
+                                             "node": {
+                                                "description": "The cluster node name.",
+                                                "format": "pve-node",
+                                                "type": "string"
+                                             },
+                                             "vnet": {
+                                                "completion": ("Code")[],
+                                                "description": "The SDN vnet object identifier.",
+                                                "format": "pve-sdn-vnet-id",
+                                                "type": "string"
+                                             }
+                                          }
+                                       },
+                                       "permissions": {
+                                          "description": "Require 'SDN.Audit' permissions on '/sdn/zones/<zone>/<vnet>'",
+                                          "user": "all"
+                                       },
+                                       "returns": {
+                                          "items": {
+                                             "properties": {
+                                                "subdir": {
+                                                   "type": "string"
+                                                }
+                                             },
+                                             "type": "object"
+                                          },
+                                          "links": [
+                                             {
+                                                "href": "{subdir}",
+                                                "rel": "child"
+                                             }
+                                          ],
+                                          "type": "array"
+                                       }
+                                    }
+                                 },
+                                 "leaf": 0,
+                                 "path": "/nodes/{node}/sdn/vnets/{vnet}",
+                                 "text": "{vnet}"
+                              }
+                           ],
+                           "leaf": 0,
+                           "path": "/nodes/{node}/sdn/vnets",
+                           "text": "vnets"
                         }
                      ],
                      "info": {
@@ -50874,6 +51151,7 @@
                            "permissions": {
                               "user": "all"
                            },
+                           "proxyto": "node",
                            "returns": {
                               "items": {
                                  "properties": {},
-- 
2.47.3


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


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

* [pdm-devel] [PATCH proxmox 2/3] pve-api-types: generate ip-vrf / mac-vrf endpoints
  2025-11-07  8:59 [pdm-devel] [PATCH proxmox{, -datacenter-manager} 0/8] Integration of IP-VRF and MAC-VRF status to EVPN panel Stefan Hanreich
  2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox 1/3] pve-api-types: add zone / vnet status reporting endpoints Stefan Hanreich
@ 2025-11-07  8:59 ` Stefan Hanreich
  2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox 3/3] pve-api-types: regenerate Stefan Hanreich
                   ` (5 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Stefan Hanreich @ 2025-11-07  8:59 UTC (permalink / raw)
  To: pdm-devel

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

diff --git a/pve-api-types/generate.pl b/pve-api-types/generate.pl
index 6eecba6d..5cfb4f78 100644
--- a/pve-api-types/generate.pl
+++ b/pve-api-types/generate.pl
@@ -368,6 +368,8 @@ 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');
+api(GET => '/nodes/{node}/sdn/zones/{zone}/ip-vrf', 'get_zone_ip_vrf', 'return-name' => 'SdnZoneIpVrf');
+Schema2Rust::derive('SdnZoneIpVrf' => 'Clone', 'PartialEq');
 
 api(GET => '/cluster/sdn/controllers', 'list_controllers', 'return-name' => 'SdnController');
 Schema2Rust::derive('SdnController' => 'Clone', 'PartialEq');
@@ -380,6 +382,8 @@ 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');
+api(GET => '/nodes/{node}/sdn/vnets/{vnet}/mac-vrf', 'get_vnet_mac_vrf', 'return-name' => 'SdnVnetMacVrf');
+Schema2Rust::derive('SdnVnetMacVrf' => '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');
-- 
2.47.3


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


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

* [pdm-devel] [PATCH proxmox 3/3] pve-api-types: regenerate
  2025-11-07  8:59 [pdm-devel] [PATCH proxmox{, -datacenter-manager} 0/8] Integration of IP-VRF and MAC-VRF status to EVPN panel Stefan Hanreich
  2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox 1/3] pve-api-types: add zone / vnet status reporting endpoints Stefan Hanreich
  2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox 2/3] pve-api-types: generate ip-vrf / mac-vrf endpoints Stefan Hanreich
@ 2025-11-07  8:59 ` Stefan Hanreich
  2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox-datacenter-manager 1/5] server: api: sdn: add ip-vrf endpoint Stefan Hanreich
                   ` (4 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Stefan Hanreich @ 2025-11-07  8:59 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 pve-api-types/src/generated/code.rs  |  25 +++++++
 pve-api-types/src/generated/types.rs | 104 +++++++++++++++++++++++----
 2 files changed, 116 insertions(+), 13 deletions(-)

diff --git a/pve-api-types/src/generated/code.rs b/pve-api-types/src/generated/code.rs
index 07728f3f..0f73aa97 100644
--- a/pve-api-types/src/generated/code.rs
+++ b/pve-api-types/src/generated/code.rs
@@ -329,8 +329,11 @@
 /// - /nodes/{node}/scan/pbs
 /// - /nodes/{node}/scan/zfs
 /// - /nodes/{node}/sdn
+/// - /nodes/{node}/sdn/vnets
+/// - /nodes/{node}/sdn/vnets/{vnet}
 /// - /nodes/{node}/sdn/zones
 /// - /nodes/{node}/sdn/zones/{zone}
+/// - /nodes/{node}/sdn/zones/{zone}/bridges
 /// - /nodes/{node}/sdn/zones/{zone}/content
 /// - /nodes/{node}/services
 /// - /nodes/{node}/services/{service}
@@ -472,6 +475,16 @@ pub trait PveClient {
         Err(Error::Other("get_task_status not implemented"))
     }
 
+    /// Get the MAC VRF for a VNet in an EVPN zone.
+    async fn get_vnet_mac_vrf(&self, node: &str, vnet: &str) -> Result<Vec<SdnVnetMacVrf>, Error> {
+        Err(Error::Other("get_vnet_mac_vrf not implemented"))
+    }
+
+    /// Get the IP VRF of an EVPN zone.
+    async fn get_zone_ip_vrf(&self, node: &str, zone: &str) -> Result<Vec<SdnZoneIpVrf>, Error> {
+        Err(Error::Other("get_zone_ip_vrf not implemented"))
+    }
+
     /// List available updates.
     async fn list_available_updates(&self, node: &str) -> Result<Vec<AptUpdateInfo>, Error> {
         Err(Error::Other("list_available_updates not implemented"))
@@ -974,6 +987,18 @@ where
         Ok(self.0.get(url).await?.expect_json()?.data)
     }
 
+    /// Get the MAC VRF for a VNet in an EVPN zone.
+    async fn get_vnet_mac_vrf(&self, node: &str, vnet: &str) -> Result<Vec<SdnVnetMacVrf>, Error> {
+        let url = &format!("/api2/extjs/nodes/{node}/sdn/vnets/{vnet}/mac-vrf");
+        Ok(self.0.get(url).await?.expect_json()?.data)
+    }
+
+    /// Get the IP VRF of an EVPN zone.
+    async fn get_zone_ip_vrf(&self, node: &str, zone: &str) -> Result<Vec<SdnZoneIpVrf>, Error> {
+        let url = &format!("/api2/extjs/nodes/{node}/sdn/zones/{zone}/ip-vrf");
+        Ok(self.0.get(url).await?.expect_json()?.data)
+    }
+
     /// List available updates.
     async fn list_available_updates(&self, node: &str) -> Result<Vec<AptUpdateInfo>, Error> {
         let url = &format!("/api2/extjs/nodes/{node}/apt/update");
diff --git a/pve-api-types/src/generated/types.rs b/pve-api-types/src/generated/types.rs
index 6c42b620..5d55168f 100644
--- a/pve-api-types/src/generated/types.rs
+++ b/pve-api-types/src/generated/types.rs
@@ -11920,6 +11920,46 @@ pub struct SdnVnet {
     pub zone: Option<String>,
 }
 
+const_regex! {
+
+SDN_VNET_MAC_VRF_MAC_RE = r##"^(?i)[a-f0-9][02468ace](?::[a-f0-9]{2}){5}$"##;
+
+}
+
+#[test]
+fn test_regex_compilation_31() {
+    use regex::Regex;
+    let _: &Regex = &SDN_VNET_MAC_VRF_MAC_RE;
+}
+#[api(
+    properties: {
+        ip: {
+            format: &ApiStringFormat::VerifyFn(verifiers::verify_ip),
+            type: String,
+        },
+        mac: {
+            format: &ApiStringFormat::Pattern(&SDN_VNET_MAC_VRF_MAC_RE),
+            type: String,
+        },
+        nexthop: {
+            format: &ApiStringFormat::VerifyFn(verifiers::verify_ip),
+            type: String,
+        },
+    },
+)]
+/// Object.
+#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
+pub struct SdnVnetMacVrf {
+    /// The IP address of the MAC VRF entry.
+    pub ip: String,
+
+    /// The MAC address of the MAC VRF entry.
+    pub mac: String,
+
+    /// The IP address of the nexthop.
+    pub nexthop: String,
+}
+
 #[api(
     properties: {
         alias: {
@@ -11996,7 +12036,7 @@ SDN_ZONE_EXITNODES_PRIMARY_RE = r##"^(?i:[a-z0-9](?i:[a-z0-9\-]*[a-z0-9])?)$"##;
 }
 
 #[test]
-fn test_regex_compilation_31() {
+fn test_regex_compilation_32() {
     use regex::Regex;
     let _: &Regex = &SDN_ZONE_EXITNODES_RE;
     let _: &Regex = &SDN_ZONE_EXITNODES_PRIMARY_RE;
@@ -12262,6 +12302,44 @@ pub enum SdnZoneDhcp {
 serde_plain::derive_display_from_serialize!(SdnZoneDhcp);
 serde_plain::derive_fromstr_from_deserialize!(SdnZoneDhcp);
 
+#[api(
+    properties: {
+        ip: {
+            format: &ApiStringFormat::VerifyFn(verifiers::verify_cidr),
+            type: String,
+        },
+        metric: {
+            type: Integer,
+        },
+        nexthops: {
+            items: {
+                description: "the interface name or ip address of the next hop",
+                type: String,
+            },
+            type: Array,
+        },
+        protocol: {
+            type: String,
+        },
+    },
+)]
+/// Object.
+#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
+pub struct SdnZoneIpVrf {
+    /// The CIDR of the route table entry.
+    pub ip: String,
+
+    /// This route's metric.
+    #[serde(deserialize_with = "proxmox_serde::perl::deserialize_i64")]
+    pub metric: i64,
+
+    /// A list of nexthops for the route table entry.
+    pub nexthops: Vec<String>,
+
+    /// The protocol where this route was learned from (e.g. BGP).
+    pub protocol: String,
+}
+
 const_regex! {
 
 SDN_ZONE_PENDING_EXITNODES_RE = r##"^(?i:[a-z0-9](?i:[a-z0-9\-]*[a-z0-9])?)$"##;
@@ -12270,7 +12348,7 @@ SDN_ZONE_PENDING_EXITNODES_PRIMARY_RE = r##"^(?i:[a-z0-9](?i:[a-z0-9\-]*[a-z0-9]
 }
 
 #[test]
-fn test_regex_compilation_32() {
+fn test_regex_compilation_33() {
     use regex::Regex;
     let _: &Regex = &SDN_ZONE_PENDING_EXITNODES_RE;
     let _: &Regex = &SDN_ZONE_PENDING_EXITNODES_PRIMARY_RE;
@@ -12600,7 +12678,7 @@ START_QEMU_TARGETSTORAGE_RE = r##"^(?i:[a-z][a-z0-9\-_.]*[a-z0-9]):(?i:[a-z][a-z
 }
 
 #[test]
-fn test_regex_compilation_33() {
+fn test_regex_compilation_34() {
     use regex::Regex;
     let _: &Regex = &START_QEMU_MIGRATEDFROM_RE;
     let _: &Regex = &START_QEMU_TARGETSTORAGE_RE;
@@ -12771,7 +12849,7 @@ STOP_QEMU_MIGRATEDFROM_RE = r##"^(?i:[a-z0-9](?i:[a-z0-9\-]*[a-z0-9])?)$"##;
 }
 
 #[test]
-fn test_regex_compilation_34() {
+fn test_regex_compilation_35() {
     use regex::Regex;
     let _: &Regex = &STOP_QEMU_MIGRATEDFROM_RE;
 }
@@ -12870,7 +12948,7 @@ STORAGE_INFO_STORAGE_RE = r##"^(?i:[a-z][a-z0-9\-_.]*[a-z0-9])$"##;
 }
 
 #[test]
-fn test_regex_compilation_35() {
+fn test_regex_compilation_36() {
     use regex::Regex;
     let _: &Regex = &STORAGE_INFO_STORAGE_RE;
 }
@@ -13209,7 +13287,7 @@ UPDATE_QEMU_CONFIG_VMSTATESTORAGE_RE = r##"^(?i:[a-z][a-z0-9\-_.]*[a-z0-9])$"##;
 }
 
 #[test]
-fn test_regex_compilation_36() {
+fn test_regex_compilation_37() {
     use regex::Regex;
     let _: &Regex = &UPDATE_QEMU_CONFIG_AFFINITY_RE;
     let _: &Regex = &UPDATE_QEMU_CONFIG_BOOTDISK_RE;
@@ -14100,7 +14178,7 @@ UPDATE_QEMU_CONFIG_ASYNC_VMSTATESTORAGE_RE = r##"^(?i:[a-z][a-z0-9\-_.]*[a-z0-9]
 }
 
 #[test]
-fn test_regex_compilation_37() {
+fn test_regex_compilation_38() {
     use regex::Regex;
     let _: &Regex = &UPDATE_QEMU_CONFIG_ASYNC_AFFINITY_RE;
     let _: &Regex = &UPDATE_QEMU_CONFIG_ASYNC_BOOTDISK_RE;
@@ -14969,7 +15047,7 @@ UPDATE_QEMU_CONFIG_EFIDISK0_SIZE_RE = r##"^(\d+(\.\d+)?)([KMGT])?$"##;
 }
 
 #[test]
-fn test_regex_compilation_38() {
+fn test_regex_compilation_39() {
     use regex::Regex;
     let _: &Regex = &UPDATE_QEMU_CONFIG_EFIDISK0_SIZE_RE;
 }
@@ -15046,7 +15124,7 @@ UPDATE_QEMU_CONFIG_IDE_SIZE_RE = r##"^(\d+(\.\d+)?)([KMGT])?$"##;
 }
 
 #[test]
-fn test_regex_compilation_39() {
+fn test_regex_compilation_40() {
     use regex::Regex;
     let _: &Regex = &UPDATE_QEMU_CONFIG_IDE_MODEL_RE;
     let _: &Regex = &UPDATE_QEMU_CONFIG_IDE_SERIAL_RE;
@@ -15402,7 +15480,7 @@ UPDATE_QEMU_CONFIG_SATA_SIZE_RE = r##"^(\d+(\.\d+)?)([KMGT])?$"##;
 }
 
 #[test]
-fn test_regex_compilation_40() {
+fn test_regex_compilation_41() {
     use regex::Regex;
     let _: &Regex = &UPDATE_QEMU_CONFIG_SATA_SERIAL_RE;
     let _: &Regex = &UPDATE_QEMU_CONFIG_SATA_SIZE_RE;
@@ -15747,7 +15825,7 @@ UPDATE_QEMU_CONFIG_SCSI_SIZE_RE = r##"^(\d+(\.\d+)?)([KMGT])?$"##;
 }
 
 #[test]
-fn test_regex_compilation_41() {
+fn test_regex_compilation_42() {
     use regex::Regex;
     let _: &Regex = &UPDATE_QEMU_CONFIG_SCSI_SERIAL_RE;
     let _: &Regex = &UPDATE_QEMU_CONFIG_SCSI_SIZE_RE;
@@ -16147,7 +16225,7 @@ UPDATE_QEMU_CONFIG_TPMSTATE0_SIZE_RE = r##"^(\d+(\.\d+)?)([KMGT])?$"##;
 }
 
 #[test]
-fn test_regex_compilation_42() {
+fn test_regex_compilation_43() {
     use regex::Regex;
     let _: &Regex = &UPDATE_QEMU_CONFIG_TPMSTATE0_SIZE_RE;
 }
@@ -16203,7 +16281,7 @@ UPDATE_QEMU_CONFIG_VIRTIO_SIZE_RE = r##"^(\d+(\.\d+)?)([KMGT])?$"##;
 }
 
 #[test]
-fn test_regex_compilation_43() {
+fn test_regex_compilation_44() {
     use regex::Regex;
     let _: &Regex = &UPDATE_QEMU_CONFIG_VIRTIO_SERIAL_RE;
     let _: &Regex = &UPDATE_QEMU_CONFIG_VIRTIO_SIZE_RE;
-- 
2.47.3


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


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

* [pdm-devel] [PATCH proxmox-datacenter-manager 1/5] server: api: sdn: add ip-vrf endpoint
  2025-11-07  8:59 [pdm-devel] [PATCH proxmox{, -datacenter-manager} 0/8] Integration of IP-VRF and MAC-VRF status to EVPN panel Stefan Hanreich
                   ` (2 preceding siblings ...)
  2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox 3/3] pve-api-types: regenerate Stefan Hanreich
@ 2025-11-07  8:59 ` Stefan Hanreich
  2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox-datacenter-manager 2/5] server: api: sdn: add mac-vrf endpoint Stefan Hanreich
                   ` (3 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Stefan Hanreich @ 2025-11-07  8:59 UTC (permalink / raw)
  To: pdm-devel

Calls the respective Proxmox VE endpoint to obtain status information
about the IP-VRF of an EVPN zone. Since the status is per-node, use
the existing nodes subdirectory instead of the general SDN
subdirectory, mirroring the API paths used in Proxmox VE. In order to
avoid having too many SDN-specific modules for a single API call,
introduce a submodule in the sdn module directly. If needed, this can
be easily factored out in the future.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 lib/pdm-client/src/lib.rs   | 12 ++++++++
 server/src/api/nodes/mod.rs |  2 ++
 server/src/api/nodes/sdn.rs | 58 +++++++++++++++++++++++++++++++++++++
 server/src/api/pve/node.rs  |  3 +-
 4 files changed, 74 insertions(+), 1 deletion(-)
 create mode 100644 server/src/api/nodes/sdn.rs

diff --git a/lib/pdm-client/src/lib.rs b/lib/pdm-client/src/lib.rs
index 0cab769..e22d139 100644
--- a/lib/pdm-client/src/lib.rs
+++ b/lib/pdm-client/src/lib.rs
@@ -66,6 +66,8 @@ pub mod types {
     pub use pve_api_types::{ListControllersType, ListZonesType, SdnObjectState};
 
     pub use pve_api_types::StorageStatus as PveStorageStatus;
+
+    pub use pve_api_types::SdnZoneIpVrf;
 }
 
 pub struct PdmClient<T: HttpApiClient>(pub T);
@@ -1051,6 +1053,16 @@ impl<T: HttpApiClient> PdmClient<T> {
         Ok(self.0.post(path, &params).await?.expect_json()?.data)
     }
 
+    pub async fn pve_sdn_zone_get_ip_vrf(
+        &self,
+        remote: &str,
+        node: &str,
+        zone: &str,
+    ) -> Result<Vec<SdnZoneIpVrf>, Error> {
+        let path = format!("/api2/extjs/pve/remotes/{remote}/nodes/{node}/sdn/zones/{zone}/ip-vrf");
+        Ok(self.0.get(&path).await?.expect_json()?.data)
+    }
+
     /// uses /pbs/probe-tls to probe the tls connection to the given host
     pub async fn pbs_probe_tls(
         &self,
diff --git a/server/src/api/nodes/mod.rs b/server/src/api/nodes/mod.rs
index 6f30ba7..66f88f3 100644
--- a/server/src/api/nodes/mod.rs
+++ b/server/src/api/nodes/mod.rs
@@ -10,6 +10,7 @@ pub mod dns;
 pub mod journal;
 pub mod network;
 pub mod rrddata;
+pub mod sdn;
 pub mod syslog;
 pub mod tasks;
 pub mod termproxy;
@@ -45,6 +46,7 @@ pub const SUBDIRS: SubdirMap = &sorted!([
     ("journal", &journal::ROUTER),
     ("network", &network::ROUTER),
     ("rrdata", &rrddata::ROUTER),
+    ("sdn", &sdn::ROUTER),
     ("syslog", &syslog::ROUTER),
     ("tasks", &tasks::ROUTER),
     ("termproxy", &termproxy::ROUTER),
diff --git a/server/src/api/nodes/sdn.rs b/server/src/api/nodes/sdn.rs
new file mode 100644
index 0000000..9ca6130
--- /dev/null
+++ b/server/src/api/nodes/sdn.rs
@@ -0,0 +1,58 @@
+use anyhow::{anyhow, Error};
+use http::StatusCode;
+
+use pdm_api_types::{remotes::REMOTE_ID_SCHEMA, sdn::SDN_ID_SCHEMA, NODE_SCHEMA};
+use proxmox_router::{list_subdirs_api_method, Router, SubdirMap};
+use proxmox_schema::api;
+use pve_api_types::SdnZoneIpVrf;
+
+use crate::api::pve::{connect, get_remote};
+
+mod zones {
+    use super::*;
+
+    const ZONE_SUBDIRS: SubdirMap = &[("ip-vrf", &Router::new().get(&API_METHOD_GET_IP_VRF))];
+
+    const ZONE_ROUTER: Router = Router::new()
+        .get(&list_subdirs_api_method!(ZONE_SUBDIRS))
+        .subdirs(ZONE_SUBDIRS);
+
+    pub const ROUTER: Router = Router::new().match_all("zone", &ZONE_ROUTER);
+
+    #[api(
+        input: {
+            properties: {
+                remote: { schema: REMOTE_ID_SCHEMA },
+                node: { schema: NODE_SCHEMA },
+                zone: { schema: SDN_ID_SCHEMA },
+            },
+        },
+        returns: { type: SdnZoneIpVrf },
+    )]
+    /// Get the IP-VRF for an EVPN zone for a node on a given remote
+    async fn get_ip_vrf(
+        remote: String,
+        node: String,
+        zone: String,
+    ) -> Result<Vec<SdnZoneIpVrf>, Error> {
+        let (remote_config, _) = pdm_config::remotes::config()?;
+        let remote = get_remote(&remote_config, &remote)?;
+        let client = connect(remote)?;
+
+        client
+            .get_zone_ip_vrf(&node, &zone)
+            .await
+            .map_err(|err| match err {
+                proxmox_client::Error::Api(StatusCode::NOT_IMPLEMENTED, _msg) => {
+                    anyhow!("remote {} does not support the zone ip-vrf API call, please upgrade to the newest version!", remote.id)
+                }
+                _ => err.into()
+            })
+    }
+}
+
+const SUBDIRS: SubdirMap = &[("zone", &zones::ROUTER)];
+
+pub const ROUTER: Router = Router::new()
+    .get(&list_subdirs_api_method!(SUBDIRS))
+    .subdirs(SUBDIRS);
diff --git a/server/src/api/pve/node.rs b/server/src/api/pve/node.rs
index 301c0b1..dae890e 100644
--- a/server/src/api/pve/node.rs
+++ b/server/src/api/pve/node.rs
@@ -7,7 +7,7 @@ use proxmox_sortable_macro::sortable;
 use pdm_api_types::{remotes::REMOTE_ID_SCHEMA, NODE_SCHEMA, PRIV_RESOURCE_AUDIT};
 use pve_api_types::StorageContent;
 
-use crate::api::pve::storage;
+use crate::api::{nodes::sdn, pve::storage};
 
 pub const ROUTER: Router = Router::new()
     .get(&list_subdirs_api_method!(SUBDIRS))
@@ -18,6 +18,7 @@ const SUBDIRS: SubdirMap = &sorted!([
     ("apt", &crate::api::remote_updates::APT_ROUTER),
     ("rrddata", &super::rrddata::NODE_RRD_ROUTER),
     ("network", &Router::new().get(&API_METHOD_GET_NETWORK)),
+    ("sdn", &sdn::ROUTER),
     ("storage", &STORAGE_ROUTER),
     ("status", &Router::new().get(&API_METHOD_GET_STATUS)),
 ]);
-- 
2.47.3


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


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

* [pdm-devel] [PATCH proxmox-datacenter-manager 2/5] server: api: sdn: add mac-vrf endpoint
  2025-11-07  8:59 [pdm-devel] [PATCH proxmox{, -datacenter-manager} 0/8] Integration of IP-VRF and MAC-VRF status to EVPN panel Stefan Hanreich
                   ` (3 preceding siblings ...)
  2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox-datacenter-manager 1/5] server: api: sdn: add ip-vrf endpoint Stefan Hanreich
@ 2025-11-07  8:59 ` Stefan Hanreich
  2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox-datacenter-manager 3/5] ui: sdn: evpn: add zone status panel Stefan Hanreich
                   ` (2 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Stefan Hanreich @ 2025-11-07  8:59 UTC (permalink / raw)
  To: pdm-devel

Calls the respective Proxmox VE endpoint to obtain status information
about the MAC-VRF of an EVPN vnet. Since the status is per-node, use
the existing nodes subdirectory instead of the general SDN
subdirectory, mirroring the API path from Proxmox VE.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 lib/pdm-client/src/lib.rs   | 13 +++++++++-
 server/src/api/nodes/sdn.rs | 47 +++++++++++++++++++++++++++++++++++--
 2 files changed, 57 insertions(+), 3 deletions(-)

diff --git a/lib/pdm-client/src/lib.rs b/lib/pdm-client/src/lib.rs
index e22d139..3b0af86 100644
--- a/lib/pdm-client/src/lib.rs
+++ b/lib/pdm-client/src/lib.rs
@@ -67,7 +67,7 @@ pub mod types {
 
     pub use pve_api_types::StorageStatus as PveStorageStatus;
 
-    pub use pve_api_types::SdnZoneIpVrf;
+    pub use pve_api_types::{SdnVnetMacVrf, SdnZoneIpVrf};
 }
 
 pub struct PdmClient<T: HttpApiClient>(pub T);
@@ -1063,6 +1063,17 @@ impl<T: HttpApiClient> PdmClient<T> {
         Ok(self.0.get(&path).await?.expect_json()?.data)
     }
 
+    pub async fn pve_sdn_vnet_get_mac_vrf(
+        &self,
+        remote: &str,
+        node: &str,
+        vnet: &str,
+    ) -> Result<Vec<SdnVnetMacVrf>, Error> {
+        let path =
+            format!("/api2/extjs/pve/remotes/{remote}/nodes/{node}/sdn/vnets/{vnet}/mac-vrf");
+        Ok(self.0.get(&path).await?.expect_json()?.data)
+    }
+
     /// uses /pbs/probe-tls to probe the tls connection to the given host
     pub async fn pbs_probe_tls(
         &self,
diff --git a/server/src/api/nodes/sdn.rs b/server/src/api/nodes/sdn.rs
index 9ca6130..065ebe0 100644
--- a/server/src/api/nodes/sdn.rs
+++ b/server/src/api/nodes/sdn.rs
@@ -4,7 +4,7 @@ use http::StatusCode;
 use pdm_api_types::{remotes::REMOTE_ID_SCHEMA, sdn::SDN_ID_SCHEMA, NODE_SCHEMA};
 use proxmox_router::{list_subdirs_api_method, Router, SubdirMap};
 use proxmox_schema::api;
-use pve_api_types::SdnZoneIpVrf;
+use pve_api_types::{SdnVnetMacVrf, SdnZoneIpVrf};
 
 use crate::api::pve::{connect, get_remote};
 
@@ -51,7 +51,50 @@ mod zones {
     }
 }
 
-const SUBDIRS: SubdirMap = &[("zone", &zones::ROUTER)];
+mod vnets {
+    use super::*;
+
+    const VNET_SUBDIRS: SubdirMap = &[("mac-vrf", &Router::new().get(&API_METHOD_GET_MAC_VRF))];
+
+    const VNET_ROUTER: Router = Router::new()
+        .get(&list_subdirs_api_method!(VNET_SUBDIRS))
+        .subdirs(VNET_SUBDIRS);
+
+    pub const ROUTER: Router = Router::new().match_all("vnet", &VNET_ROUTER);
+
+    #[api(
+        input: {
+            properties: {
+                remote: { schema: REMOTE_ID_SCHEMA },
+                node: { schema: NODE_SCHEMA },
+                vnet: { schema: SDN_ID_SCHEMA },
+            },
+        },
+        returns: { type: SdnVnetMacVrf },
+    )]
+    /// Get the MAC-VRF for an EVPN vnet for a node on a given remote
+    async fn get_mac_vrf(
+        remote: String,
+        node: String,
+        vnet: String,
+    ) -> Result<Vec<SdnVnetMacVrf>, Error> {
+        let (remote_config, _) = pdm_config::remotes::config()?;
+        let remote = get_remote(&remote_config, &remote)?;
+        let client = connect(&remote)?;
+
+        client
+            .get_vnet_mac_vrf(&node, &vnet)
+            .await
+            .map_err(|err| match err {
+                proxmox_client::Error::Api(StatusCode::NOT_IMPLEMENTED, _msg) => {
+                    anyhow!("remote {} does not support the vnet mac-vrf API call, please upgrade to the newest version!", remote.id)
+                }
+                _ => err.into()
+            })
+    }
+}
+
+const SUBDIRS: SubdirMap = &[("vnets", &vnets::ROUTER), ("zones", &zones::ROUTER)];
 
 pub const ROUTER: Router = Router::new()
     .get(&list_subdirs_api_method!(SUBDIRS))
-- 
2.47.3


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


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

* [pdm-devel] [PATCH proxmox-datacenter-manager 3/5] ui: sdn: evpn: add zone status panel
  2025-11-07  8:59 [pdm-devel] [PATCH proxmox{, -datacenter-manager} 0/8] Integration of IP-VRF and MAC-VRF status to EVPN panel Stefan Hanreich
                   ` (4 preceding siblings ...)
  2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox-datacenter-manager 2/5] server: api: sdn: add mac-vrf endpoint Stefan Hanreich
@ 2025-11-07  8:59 ` Stefan Hanreich
  2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox-datacenter-manager 4/5] ui: sdn: evpn: add vnet " Stefan Hanreich
  2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox-datacenter-manager 5/5] sdn: evpn: add detail panel to the evpn panel Stefan Hanreich
  7 siblings, 0 replies; 10+ messages in thread
From: Stefan Hanreich @ 2025-11-07  8:59 UTC (permalink / raw)
  To: pdm-devel

This panel shows the status of the IP-VRF of an EVPN zone. It lists
the routing table of a given node, which can be selected via the
dropdown in the panel. It will be used in the remote tree of the EVPN
view to display detailed information about the selected EVPN zone.

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

diff --git a/ui/src/sdn/evpn/mod.rs b/ui/src/sdn/evpn/mod.rs
index 1948ecf..6c919ba 100644
--- a/ui/src/sdn/evpn/mod.rs
+++ b/ui/src/sdn/evpn/mod.rs
@@ -13,6 +13,9 @@ pub use add_vnet::AddVnetWindow;
 mod add_zone;
 pub use add_zone::AddZoneWindow;
 
+mod zone_status;
+pub use zone_status::ZoneStatusTable;
+
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)]
 pub struct EvpnRouteTarget {
     asn: u32,
@@ -39,3 +42,33 @@ impl std::fmt::Display for EvpnRouteTarget {
         write!(f, "{}:{}", self.asn, self.vni)
     }
 }
+
+#[derive(Debug, Clone, PartialEq, Default)]
+#[repr(transparent)]
+pub struct NodeList(Vec<String>);
+
+impl std::ops::Deref for NodeList {
+    type Target = Vec<String>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl std::str::FromStr for NodeList {
+    type Err = anyhow::Error;
+
+    fn from_str(value: &str) -> Result<Self, Self::Err> {
+        if value.is_empty() {
+            anyhow::bail!("node list cannot be an empty string");
+        }
+
+        Ok(Self(value.split(",").map(String::from).collect()))
+    }
+}
+
+impl FromIterator<String> for NodeList {
+    fn from_iter<I: IntoIterator<Item = String>>(iter: I) -> Self {
+        Self(iter.into_iter().collect())
+    }
+}
diff --git a/ui/src/sdn/evpn/zone_status.rs b/ui/src/sdn/evpn/zone_status.rs
new file mode 100644
index 0000000..a78fd7d
--- /dev/null
+++ b/ui/src/sdn/evpn/zone_status.rs
@@ -0,0 +1,261 @@
+use std::cmp::Ordering;
+use std::future::Future;
+use std::pin::Pin;
+use std::rc::Rc;
+
+use anyhow::{Context, Error};
+use proxmox_yew_comp::{LoadableComponent, LoadableComponentContext, LoadableComponentMaster};
+use pwt::props::{EventSubscriber, ExtractPrimaryKey};
+use yew::virtual_dom::{Key, VComp, VNode};
+use yew::{html, AttrValue, Properties};
+
+use pdm_client::types::SdnZoneIpVrf;
+use pwt::props::{ContainerBuilder, FieldBuilder, WidgetBuilder, WidgetStyleBuilder};
+use pwt::state::Store;
+use pwt::tr;
+use pwt::widget::data_table::{DataTable, DataTableColumn, DataTableHeader};
+use pwt::widget::form::Combobox;
+use pwt::widget::{error_message, Button, Column, InputPanel, Toolbar};
+
+use crate::pdm_client;
+use crate::sdn::evpn::NodeList;
+
+#[derive(Clone, PartialEq, Properties, Default)]
+pub struct ZoneStatusTable {
+    remote: String,
+    zone: String,
+    nodes: Option<NodeList>,
+}
+
+impl ZoneStatusTable {
+    pub fn new(remote: String, zone: String, nodes: Option<NodeList>) -> Self {
+        yew::props!(Self {
+            zone,
+            remote,
+            nodes
+        })
+    }
+}
+
+impl From<ZoneStatusTable> for VNode {
+    fn from(value: ZoneStatusTable) -> Self {
+        let comp = VComp::new::<LoadableComponentMaster<ZoneStatusComponent>>(Rc::new(value), None);
+        VNode::from(comp)
+    }
+}
+
+#[derive(Clone, PartialEq)]
+#[repr(transparent)]
+pub struct IpVrfEntry(pub SdnZoneIpVrf);
+
+impl From<SdnZoneIpVrf> for IpVrfEntry {
+    fn from(value: SdnZoneIpVrf) -> Self {
+        Self(value)
+    }
+}
+
+impl ExtractPrimaryKey for IpVrfEntry {
+    fn extract_key(&self) -> Key {
+        Key::from(self.0.ip.as_str())
+    }
+}
+
+fn default_sorter(a: &IpVrfEntry, b: &IpVrfEntry) -> Ordering {
+    (&a.0.ip, &a.0.metric).cmp(&(&b.0.ip, &b.0.metric))
+}
+
+pub struct ZoneStatusComponent {
+    store: Store<IpVrfEntry>,
+    columns: Rc<Vec<DataTableHeader<IpVrfEntry>>>,
+    nodes: Option<Rc<Vec<AttrValue>>>,
+    selected_node: Option<AttrValue>,
+    error_msg: Option<String>,
+    vrf_loading: bool,
+}
+
+impl ZoneStatusComponent {
+    fn columns() -> Rc<Vec<DataTableHeader<IpVrfEntry>>> {
+        Rc::new(vec![
+            DataTableColumn::new(tr!("Destination"))
+                .get_property(|entry: &IpVrfEntry| &entry.0.ip)
+                .into(),
+            DataTableColumn::new(tr!("Nexthops"))
+                .render(|entry: &IpVrfEntry| {
+                    let mut column = Column::new();
+
+                    for nexthop in &entry.0.nexthops {
+                        column.add_child(html! { <div>{ nexthop }</div> });
+                    }
+
+                    column.into()
+                })
+                .into(),
+            DataTableColumn::new(tr!("Protocol"))
+                .get_property(|entry: &IpVrfEntry| &entry.0.protocol)
+                .into(),
+            DataTableColumn::new(tr!("Metric"))
+                .get_property(|entry: &IpVrfEntry| &entry.0.metric)
+                .into(),
+        ])
+    }
+}
+
+#[derive(Debug)]
+pub enum ZoneStatusComponentMsg {
+    NodeSelected(Option<String>),
+    NodeListLoaded(Rc<Vec<AttrValue>>),
+    ZoneStatusLoaded(Result<Vec<SdnZoneIpVrf>, Error>),
+}
+
+impl LoadableComponent for ZoneStatusComponent {
+    type Message = ZoneStatusComponentMsg;
+    type Properties = ZoneStatusTable;
+    type ViewState = ();
+
+    fn create(_ctx: &LoadableComponentContext<Self>) -> Self {
+        Self {
+            store: Store::new(),
+            columns: Self::columns(),
+            selected_node: None,
+            nodes: Default::default(),
+            error_msg: None,
+            vrf_loading: false,
+        }
+    }
+
+    fn load(
+        &self,
+        ctx: &proxmox_yew_comp::LoadableComponentContext<Self>,
+    ) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>> {
+        let link = ctx.link().clone();
+        let props = ctx.props().clone();
+
+        Box::pin(async move {
+            let node_list = if let Some(nodes) = props.nodes {
+                nodes.iter().cloned().map(AttrValue::from).collect()
+            } else {
+                pdm_client()
+                    .pve_list_nodes(&props.remote)
+                    .await?
+                    .into_iter()
+                    .map(|node_index| AttrValue::from(node_index.node))
+                    .collect()
+            };
+
+            link.send_message(Self::Message::NodeListLoaded(Rc::new(node_list)));
+
+            Ok(())
+        })
+    }
+
+    fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
+        match msg {
+            Self::Message::NodeListLoaded(node_list) => {
+                let selected_node = node_list.iter().next().cloned();
+
+                self.nodes = Some(node_list);
+
+                if let Some(node) = selected_node {
+                    ctx.link()
+                        .send_message(Self::Message::NodeSelected(Some(node.to_string())));
+                }
+            }
+            Self::Message::NodeSelected(node_name) => {
+                if let Some(node_name) = node_name {
+                    self.vrf_loading = true;
+                    self.selected_node = Some(node_name.clone().into());
+
+                    let link = ctx.link().clone();
+                    let props = ctx.props().clone();
+
+                    ctx.link().spawn(async move {
+                        let status_result = pdm_client()
+                            .pve_sdn_zone_get_ip_vrf(&props.remote, &node_name, &props.zone)
+                            .await;
+
+                        link.send_message(Self::Message::ZoneStatusLoaded(
+                            status_result.with_context(|| "could not load zone status".to_string()),
+                        ));
+                    });
+                }
+            }
+            Self::Message::ZoneStatusLoaded(zone_status_result) => {
+                self.vrf_loading = false;
+
+                match zone_status_result {
+                    Ok(zone_status) => {
+                        self.store
+                            .write()
+                            .set_data(zone_status.into_iter().map(IpVrfEntry::from).collect());
+
+                        self.store.set_sorter(default_sorter);
+
+                        self.error_msg = None;
+                    }
+                    Err(error) => {
+                        self.store.write().clear();
+                        self.error_msg = Some(format!("{error:?}"));
+                    }
+                }
+            }
+        }
+
+        true
+    }
+
+    fn main_view(&self, ctx: &proxmox_yew_comp::LoadableComponentContext<Self>) -> yew::Html {
+        let selected_node = self.selected_node.clone();
+
+        let toolbar = Toolbar::new()
+            .class("pwt-w-100")
+            .class("pwt-overflow-hidden")
+            .class("pwt-border-bottom")
+            .with_child(
+                InputPanel::new().with_field(
+                    tr!("Node"),
+                    Combobox::new()
+                        .min_width(100)
+                        .required(true)
+                        .value(self.selected_node.clone())
+                        .items(self.nodes.clone().unwrap_or_default())
+                        .on_change(
+                            ctx.link()
+                                .callback(|node| Self::Message::NodeSelected(Some(node))),
+                        ),
+                ),
+            )
+            .with_flex_spacer()
+            .with_child(Button::refresh(ctx.loading() || self.vrf_loading).onclick(
+                ctx.link().callback(move |_| {
+                    Self::Message::NodeSelected(selected_node.as_ref().map(ToString::to_string))
+                }),
+            ));
+
+        let table =
+            DataTable::new(self.columns.clone(), self.store.clone()).class(pwt::css::FlexFit);
+
+        let mut column = Column::new()
+            .class(pwt::css::FlexFit)
+            .with_child(toolbar)
+            .with_child(table);
+
+        if let Some(msg) = &self.error_msg {
+            column.add_child(error_message(msg));
+        }
+
+        column.into()
+    }
+
+    fn changed(
+        &mut self,
+        ctx: &LoadableComponentContext<Self>,
+        _old_props: &Self::Properties,
+    ) -> bool {
+        self.selected_node = None;
+        self.nodes = None;
+
+        ctx.link().send_reload();
+
+        true
+    }
+}
-- 
2.47.3


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


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

* [pdm-devel] [PATCH proxmox-datacenter-manager 4/5] ui: sdn: evpn: add vnet status panel
  2025-11-07  8:59 [pdm-devel] [PATCH proxmox{, -datacenter-manager} 0/8] Integration of IP-VRF and MAC-VRF status to EVPN panel Stefan Hanreich
                   ` (5 preceding siblings ...)
  2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox-datacenter-manager 3/5] ui: sdn: evpn: add zone status panel Stefan Hanreich
@ 2025-11-07  8:59 ` Stefan Hanreich
  2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox-datacenter-manager 5/5] sdn: evpn: add detail panel to the evpn panel Stefan Hanreich
  7 siblings, 0 replies; 10+ messages in thread
From: Stefan Hanreich @ 2025-11-07  8:59 UTC (permalink / raw)
  To: pdm-devel

This panel shows the status of the MAC-VRF of an EVPN vnet. It lists
the neighbor table of the vnet on a given node, which can be selected
via the dropdown in the panel. It will be used in the remote tree and
the vrf tree of the EVPN view to display detailed information about
the selected EVPN vnet.

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

diff --git a/ui/src/sdn/evpn/mod.rs b/ui/src/sdn/evpn/mod.rs
index 6c919ba..3320c0f 100644
--- a/ui/src/sdn/evpn/mod.rs
+++ b/ui/src/sdn/evpn/mod.rs
@@ -16,6 +16,9 @@ pub use add_zone::AddZoneWindow;
 mod zone_status;
 pub use zone_status::ZoneStatusTable;
 
+mod vnet_status;
+pub use vnet_status::VnetStatusTable;
+
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)]
 pub struct EvpnRouteTarget {
     asn: u32,
diff --git a/ui/src/sdn/evpn/vnet_status.rs b/ui/src/sdn/evpn/vnet_status.rs
new file mode 100644
index 0000000..6f59890
--- /dev/null
+++ b/ui/src/sdn/evpn/vnet_status.rs
@@ -0,0 +1,253 @@
+use std::cmp::Ordering;
+use std::future::Future;
+use std::pin::Pin;
+use std::rc::Rc;
+
+use anyhow::{Context, Error};
+
+use proxmox_yew_comp::{LoadableComponent, LoadableComponentContext, LoadableComponentMaster};
+use pwt::props::ExtractPrimaryKey;
+use yew::virtual_dom::{Key, VComp, VNode};
+use yew::{AttrValue, Properties};
+
+use pdm_client::types::SdnVnetMacVrf;
+use pwt::props::{
+    ContainerBuilder, EventSubscriber, FieldBuilder, WidgetBuilder, WidgetStyleBuilder,
+};
+use pwt::state::Store;
+use pwt::tr;
+use pwt::widget::data_table::{DataTable, DataTableColumn, DataTableHeader};
+use pwt::widget::form::Combobox;
+use pwt::widget::{error_message, Button, Column, InputPanel, Toolbar};
+
+use crate::pdm_client;
+use crate::sdn::evpn::NodeList;
+
+#[derive(Clone, PartialEq, Properties, Default)]
+pub struct VnetStatusTable {
+    remote: String,
+    vnet: String,
+    nodes: Option<NodeList>,
+}
+
+impl VnetStatusTable {
+    pub fn new(remote: String, vnet: String, nodes: Option<NodeList>) -> Self {
+        yew::props!(Self {
+            vnet,
+            remote,
+            nodes
+        })
+    }
+}
+
+impl From<VnetStatusTable> for VNode {
+    fn from(value: VnetStatusTable) -> Self {
+        let comp = VComp::new::<LoadableComponentMaster<VnetStatusComponent>>(Rc::new(value), None);
+        VNode::from(comp)
+    }
+}
+
+#[derive(Clone, PartialEq)]
+#[repr(transparent)]
+pub struct MacVrfEntry(pub SdnVnetMacVrf);
+
+impl From<SdnVnetMacVrf> for MacVrfEntry {
+    fn from(value: SdnVnetMacVrf) -> Self {
+        Self(value)
+    }
+}
+
+impl ExtractPrimaryKey for MacVrfEntry {
+    fn extract_key(&self) -> Key {
+        Key::from(self.0.ip.as_str())
+    }
+}
+
+fn default_sorter(a: &MacVrfEntry, b: &MacVrfEntry) -> Ordering {
+    a.0.ip.cmp(&b.0.ip)
+}
+
+pub struct VnetStatusComponent {
+    store: Store<MacVrfEntry>,
+    columns: Rc<Vec<DataTableHeader<MacVrfEntry>>>,
+    nodes: Option<Rc<Vec<AttrValue>>>,
+    selected_node: Option<AttrValue>,
+    error_msg: Option<String>,
+    vrf_loading: bool,
+}
+
+impl VnetStatusComponent {
+    fn columns() -> Rc<Vec<DataTableHeader<MacVrfEntry>>> {
+        Rc::new(vec![
+            DataTableColumn::new(tr!("IP Address"))
+                .get_property(|entry: &MacVrfEntry| &entry.0.ip)
+                .into(),
+            DataTableColumn::new(tr!("MAC Address"))
+                .get_property(|entry: &MacVrfEntry| &entry.0.mac)
+                .into(),
+            DataTableColumn::new(tr!("via"))
+                .get_property(|entry: &MacVrfEntry| &entry.0.nexthop)
+                .into(),
+        ])
+    }
+}
+
+#[derive(Debug)]
+pub enum VnetStatusComponentMsg {
+    NodeSelected(Option<String>),
+    NodeListLoaded(Rc<Vec<AttrValue>>),
+    VnetStatusLoaded(Result<Vec<SdnVnetMacVrf>, Error>),
+}
+
+impl LoadableComponent for VnetStatusComponent {
+    type Message = VnetStatusComponentMsg;
+    type Properties = VnetStatusTable;
+    type ViewState = ();
+
+    fn create(_ctx: &LoadableComponentContext<Self>) -> Self {
+        Self {
+            store: Store::new(),
+            columns: Self::columns(),
+            selected_node: None,
+            nodes: Default::default(),
+            error_msg: None,
+            vrf_loading: false,
+        }
+    }
+
+    fn load(
+        &self,
+        ctx: &proxmox_yew_comp::LoadableComponentContext<Self>,
+    ) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>> {
+        let link = ctx.link().clone();
+        let props = ctx.props().clone();
+
+        Box::pin(async move {
+            let node_list = if let Some(nodes) = props.nodes {
+                nodes.iter().cloned().map(AttrValue::from).collect()
+            } else {
+                pdm_client()
+                    .pve_list_nodes(&props.remote)
+                    .await?
+                    .into_iter()
+                    .map(|node_index| AttrValue::from(node_index.node))
+                    .collect()
+            };
+
+            link.send_message(Self::Message::NodeListLoaded(Rc::new(node_list)));
+
+            Ok(())
+        })
+    }
+
+    fn update(&mut self, ctx: &LoadableComponentContext<Self>, msg: Self::Message) -> bool {
+        match msg {
+            Self::Message::NodeListLoaded(node_list) => {
+                let selected_node = node_list.iter().next().cloned();
+
+                self.nodes = Some(node_list);
+
+                if let Some(node) = selected_node {
+                    ctx.link()
+                        .send_message(Self::Message::NodeSelected(Some(node.to_string())));
+                }
+            }
+            Self::Message::NodeSelected(node_name) => {
+                if let Some(node_name) = node_name {
+                    self.vrf_loading = true;
+                    self.selected_node = Some(node_name.clone().into());
+
+                    let link = ctx.link().clone();
+                    let props = ctx.props().clone();
+
+                    ctx.link().spawn(async move {
+                        let status_result = pdm_client()
+                            .pve_sdn_vnet_get_mac_vrf(&props.remote, &node_name, &props.vnet)
+                            .await;
+
+                        link.send_message(Self::Message::VnetStatusLoaded(
+                            status_result.with_context(|| "could not load vnet status".to_string()),
+                        ));
+                    });
+                }
+            }
+            Self::Message::VnetStatusLoaded(vnet_status_result) => {
+                self.vrf_loading = false;
+
+                match vnet_status_result {
+                    Ok(vnet_status) => {
+                        self.store
+                            .write()
+                            .set_data(vnet_status.into_iter().map(MacVrfEntry::from).collect());
+
+                        self.store.set_sorter(default_sorter);
+
+                        self.error_msg = None;
+                    }
+                    Err(error) => {
+                        self.store.write().clear();
+                        self.error_msg = Some(format!("{error:?}"));
+                    }
+                }
+            }
+        }
+
+        true
+    }
+
+    fn main_view(&self, ctx: &proxmox_yew_comp::LoadableComponentContext<Self>) -> yew::Html {
+        let selected_node = self.selected_node.clone();
+
+        let toolbar = Toolbar::new()
+            .class("pwt-w-100")
+            .class("pwt-overflow-hidden")
+            .class("pwt-border-bottom")
+            .with_child(
+                InputPanel::new().with_field(
+                    tr!("Node"),
+                    Combobox::new()
+                        .min_width(100)
+                        .required(true)
+                        .value(self.selected_node.clone())
+                        .items(self.nodes.clone().unwrap_or_default())
+                        .on_change(
+                            ctx.link()
+                                .callback(|node| Self::Message::NodeSelected(Some(node))),
+                        ),
+                ),
+            )
+            .with_flex_spacer()
+            .with_child(Button::refresh(ctx.loading() || self.vrf_loading).onclick(
+                ctx.link().callback(move |_| {
+                    Self::Message::NodeSelected(selected_node.as_ref().map(ToString::to_string))
+                }),
+            ));
+
+        let table =
+            DataTable::new(self.columns.clone(), self.store.clone()).class(pwt::css::FlexFit);
+
+        let mut column = Column::new()
+            .class(pwt::css::FlexFit)
+            .with_child(toolbar)
+            .with_child(table);
+
+        if let Some(msg) = &self.error_msg {
+            column.add_child(error_message(msg));
+        }
+
+        column.into()
+    }
+
+    fn changed(
+        &mut self,
+        ctx: &LoadableComponentContext<Self>,
+        _old_props: &Self::Properties,
+    ) -> bool {
+        self.selected_node = None;
+        self.nodes = None;
+
+        ctx.link().send_reload();
+
+        true
+    }
+}
-- 
2.47.3


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


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

* [pdm-devel] [PATCH proxmox-datacenter-manager 5/5] sdn: evpn: add detail panel to the evpn panel
  2025-11-07  8:59 [pdm-devel] [PATCH proxmox{, -datacenter-manager} 0/8] Integration of IP-VRF and MAC-VRF status to EVPN panel Stefan Hanreich
                   ` (6 preceding siblings ...)
  2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox-datacenter-manager 4/5] ui: sdn: evpn: add vnet " Stefan Hanreich
@ 2025-11-07  8:59 ` Stefan Hanreich
  7 siblings, 0 replies; 10+ messages in thread
From: Stefan Hanreich @ 2025-11-07  8:59 UTC (permalink / raw)
  To: pdm-devel

Extend the EVPN panel, so it can display detailed information about
selected elements in the trees. When selecting a specific zone / vnet,
the detail panel will show the IP / MAC-VRF of that zone / vnet. This
requires some refactoring of the existing EVPN panel to allow
displaying a second panel next to it. It is now structured like the
remote view, which also offers a panel that shows an overview of the
remote and details of the selected node.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 ui/src/sdn/evpn/evpn_panel.rs  | 129 ++++++++++++++++++++++++++++++---
 ui/src/sdn/evpn/remote_tree.rs |  71 +++++++++++++-----
 ui/src/sdn/evpn/vrf_tree.rs    |  29 +++++++-
 3 files changed, 196 insertions(+), 33 deletions(-)

diff --git a/ui/src/sdn/evpn/evpn_panel.rs b/ui/src/sdn/evpn/evpn_panel.rs
index 89e2e58..31c8795 100644
--- a/ui/src/sdn/evpn/evpn_panel.rs
+++ b/ui/src/sdn/evpn/evpn_panel.rs
@@ -1,5 +1,6 @@
 use futures::try_join;
 use std::rc::Rc;
+use std::str::FromStr;
 
 use anyhow::Error;
 use yew::virtual_dom::{VComp, VNode};
@@ -9,14 +10,20 @@ use pdm_client::types::{ListController, ListControllersType, ListVnet, ListZone,
 use proxmox_yew_comp::{LoadableComponent, LoadableComponentContext, LoadableComponentMaster};
 
 use pwt::css::{AlignItems, FlexFit, JustifyContent};
-use pwt::props::{ContainerBuilder, EventSubscriber, StorageLocation, WidgetBuilder};
-use pwt::state::NavigationContainer;
+use pwt::props::{
+    ContainerBuilder, EventSubscriber, StorageLocation, WidgetBuilder, WidgetStyleBuilder,
+};
+use pwt::state::{NavigationContainer, Selection};
 use pwt::tr;
 use pwt::widget::menu::{Menu, MenuButton, MenuItem};
-use pwt::widget::{Button, Column, MiniScrollMode, TabBarItem, TabPanel, Toolbar};
+use pwt::widget::{
+    Button, Column, Container, MiniScrollMode, Panel, Row, TabBarItem, TabPanel, Toolbar,
+};
 
 use crate::pdm_client;
-use crate::sdn::evpn::{AddVnetWindow, AddZoneWindow, RemoteTree, VrfTree};
+use crate::sdn::evpn::{
+    AddVnetWindow, AddZoneWindow, NodeList, RemoteTree, VnetStatusTable, VrfTree, ZoneStatusTable,
+};
 
 #[derive(PartialEq, Properties)]
 pub struct EvpnPanel {}
@@ -40,6 +47,11 @@ impl From<EvpnPanel> for VNode {
     }
 }
 
+pub enum DetailPanel {
+    Zone { remote: String, zone: String },
+    Vnet { remote: String, vnet: String },
+}
+
 pub enum EvpnPanelMsg {
     Reload,
     LoadFinished {
@@ -47,6 +59,7 @@ pub enum EvpnPanelMsg {
         zones: Rc<Vec<ListZone>>,
         vnets: Rc<Vec<ListVnet>>,
     },
+    DetailSelection(Option<DetailPanel>),
 }
 
 #[derive(Debug, PartialEq)]
@@ -82,6 +95,8 @@ pub struct EvpnPanelComponent {
     zones: Rc<Vec<ListZone>>,
     vnets: Rc<Vec<ListVnet>>,
     initial_load: bool,
+    selected_detail: Option<DetailPanel>,
+    selected_tab: Selection,
 }
 
 impl EvpnPanelComponent {
@@ -123,12 +138,19 @@ impl LoadableComponent for EvpnPanelComponent {
     type Message = EvpnPanelMsg;
     type ViewState = EvpnPanelViewState;
 
-    fn create(_ctx: &LoadableComponentContext<Self>) -> Self {
+    fn create(ctx: &LoadableComponentContext<Self>) -> Self {
+        let link = ctx.link().clone();
+
+        let selected_tab = Selection::new()
+            .on_select(move |_| link.send_message(Self::Message::DetailSelection(None)));
+
         Self {
             initial_load: true,
             controllers: Default::default(),
             zones: Default::default(),
             vnets: Default::default(),
+            selected_detail: None,
+            selected_tab,
         }
     }
 
@@ -166,6 +188,10 @@ impl LoadableComponent for EvpnPanelComponent {
 
                 return true;
             }
+            Self::Message::DetailSelection(data) => {
+                self.selected_detail = data;
+                return true;
+            }
             Self::Message::Reload => {
                 ctx.link().send_reload();
             }
@@ -175,11 +201,12 @@ impl LoadableComponent for EvpnPanelComponent {
     }
 
     fn main_view(&self, ctx: &LoadableComponentContext<Self>) -> Html {
-        let panel = TabPanel::new()
+        let tab_panel = TabPanel::new()
             .state_id(StorageLocation::session("EvpnPanelState"))
             .class(pwt::css::FlexFit)
             .router(true)
             .scroll_mode(MiniScrollMode::Arrow)
+            .selection(self.selected_tab.clone())
             .with_item(
                 TabBarItem::new()
                     .key("remotes")
@@ -201,6 +228,9 @@ impl LoadableComponent for EvpnPanelComponent {
                             self.zones.clone(),
                             self.vnets.clone(),
                             self.controllers.clone(),
+                            ctx.link().callback(|panel: Option<DetailPanel>| {
+                                Self::Message::DetailSelection(panel)
+                            }),
                         ))
                     }),
             )
@@ -225,16 +255,93 @@ impl LoadableComponent for EvpnPanelComponent {
                             self.zones.clone(),
                             self.vnets.clone(),
                             self.controllers.clone(),
+                            ctx.link().callback(|panel: Option<DetailPanel>| {
+                                Self::Message::DetailSelection(panel)
+                            }),
                         ))
                     }),
             );
 
-        let navigation_container = NavigationContainer::new().with_child(panel);
+        let navigation_container = NavigationContainer::new().with_child(tab_panel);
+
+        let mut container = Container::new()
+            .class("pwt-content-spacer")
+            .class(FlexFit)
+            .class("pwt-flex-direction-row")
+            .with_child(Panel::new().flex(1.0).with_child(navigation_container));
+
+        let (title, detail_html) = if let Some(detail) = &self.selected_detail {
+            match detail {
+                DetailPanel::Vnet {
+                    remote,
+                    vnet: vnet_id,
+                } => {
+                    let vnet = self.vnets.iter().find(|list_vnet| {
+                        list_vnet.vnet.vnet.as_str() == vnet_id.as_str()
+                            && list_vnet.remote.as_str() == remote
+                    });
+
+                    if let Some(vnet) = vnet {
+                        let zone = self.zones.iter().find(|list_zone| {
+                            list_zone.zone.zone.as_str() == vnet.vnet.zone.as_ref().unwrap()
+                                && list_zone.remote.as_str() == remote.as_str()
+                        });
+
+                        let node_list = zone.as_ref().and_then(|zone| {
+                            let nodes = zone.zone.nodes.as_ref()?;
+                            NodeList::from_str(nodes).ok()
+                        });
+
+                        (
+                            Some(format!("MAC-VRF for vnet '{vnet_id}' (Remote {remote})")),
+                            VnetStatusTable::new(remote.clone(), vnet_id.clone(), node_list).into(),
+                        )
+                    } else {
+                        (None, html! {"Could not find vnet {vnet_id}!"})
+                    }
+                }
+                DetailPanel::Zone {
+                    remote,
+                    zone: zone_id,
+                } => {
+                    let zone = self.zones.iter().find(|list_zone| {
+                        list_zone.zone.zone.as_str() == zone_id.as_str()
+                            && list_zone.remote.as_str() == remote.as_str()
+                    });
+
+                    let node_list = zone.as_ref().and_then(|zone| {
+                        let nodes = zone.zone.nodes.as_ref()?;
+                        NodeList::from_str(nodes).ok()
+                    });
+
+                    (
+                        Some(format!("IP-VRF for zone '{zone_id}' (Remote {remote})")),
+                        ZoneStatusTable::new(remote.clone(), zone_id.clone(), node_list).into(),
+                    )
+                }
+            }
+        } else {
+            (
+                None,
+                Row::new()
+                    .class(pwt::css::FlexFit)
+                    .class(pwt::css::JustifyContent::Center)
+                    .class(pwt::css::AlignItems::Center)
+                    .with_child(html! { tr!("Select a Zone or VNet for more details.") })
+                    .into(),
+            )
+        };
 
-        Column::new()
-            .class(pwt::css::FlexFit)
-            .with_child(navigation_container)
-            .into()
+        let mut panel = Panel::new().width(600);
+
+        if let Some(title) = title {
+            panel.set_title(title);
+        }
+
+        panel.add_child(detail_html);
+        container.add_child(panel);
+
+        container.into()
     }
 
     fn dialog_view(
diff --git a/ui/src/sdn/evpn/remote_tree.rs b/ui/src/sdn/evpn/remote_tree.rs
index 1799917..ee57b33 100644
--- a/ui/src/sdn/evpn/remote_tree.rs
+++ b/ui/src/sdn/evpn/remote_tree.rs
@@ -6,7 +6,7 @@ use std::str::FromStr;
 use anyhow::{anyhow, Error};
 use pwt::widget::{error_message, Column};
 use yew::virtual_dom::{Key, VNode};
-use yew::{Component, Context, Html, Properties};
+use yew::{Callback, Component, Context, Html, Properties};
 
 use pdm_client::types::{ListController, ListVnet, ListZone, SdnObjectState};
 use pwt::css;
@@ -19,14 +19,16 @@ use pwt::widget::data_table::{
 use pwt::widget::{Fa, Row};
 use pwt_macros::widget;
 
+use crate::sdn::evpn::evpn_panel::DetailPanel;
 use crate::sdn::evpn::EvpnRouteTarget;
 
 #[widget(comp=RemoteTreeComponent)]
-#[derive(Clone, PartialEq, Properties, Default)]
+#[derive(Clone, PartialEq, Properties)]
 pub struct RemoteTree {
     zones: Rc<Vec<ListZone>>,
     vnets: Rc<Vec<ListVnet>>,
     controllers: Rc<Vec<ListController>>,
+    on_select: Callback<Option<DetailPanel>>,
 }
 
 impl RemoteTree {
@@ -34,11 +36,13 @@ impl RemoteTree {
         zones: Rc<Vec<ListZone>>,
         vnets: Rc<Vec<ListVnet>>,
         controllers: Rc<Vec<ListController>>,
+        on_select: Callback<Option<DetailPanel>>,
     ) -> Self {
         yew::props!(Self {
             zones,
             vnets,
             controllers,
+            on_select,
         })
     }
 }
@@ -416,7 +420,34 @@ impl Component for RemoteTreeComponent {
         let store = TreeStore::new().view_root(false);
         let columns = Self::columns(store.clone());
 
-        let selection = Selection::new();
+        let on_select = ctx.props().on_select.clone();
+        let selection_store = store.clone();
+        let selection = Selection::new().on_select(move |selection: Selection| {
+            if let Some(selected_key) = selection.selected_key() {
+                let read_guard = selection_store.read();
+
+                if let Some(node) = read_guard.lookup_node(&selected_key) {
+                    match node.record() {
+                        RemoteTreeEntry::Zone(zone) => {
+                            on_select.emit(Some(DetailPanel::Zone {
+                                remote: zone.remote.clone(),
+                                zone: zone.id.clone(),
+                            }));
+                        }
+                        RemoteTreeEntry::Vnet(vnet) => {
+                            on_select.emit(Some(DetailPanel::Vnet {
+                                remote: vnet.remote.clone(),
+                                vnet: vnet.id.clone(),
+                            }));
+                        }
+                        _ => on_select.emit(None),
+                    }
+                }
+            } else {
+                on_select.emit(None);
+            }
+        });
+
         let mut error_msg = None;
 
         match zones_to_remote_view(
@@ -442,27 +473,27 @@ impl Component for RemoteTreeComponent {
     }
 
     fn view(&self, _ctx: &Context<Self>) -> Html {
-        let table = DataTable::new(self.columns.clone(), self.store.clone())
-            .striped(false)
-            .selection(self.selection.clone())
-            .row_render_callback(|args: &mut DataTableRowRenderArgs<RemoteTreeEntry>| {
-                match args.record() {
-                    RemoteTreeEntry::Vnet(vnet) if vnet.external || vnet.imported => {
-                        args.add_class("pwt-opacity-50");
-                    }
-                    RemoteTreeEntry::Remote(_) => args.add_class("pwt-bg-color-surface"),
-                    _ => (),
-                };
-            })
-            .class(css::FlexFit);
-
-        let mut column = Column::new().class(pwt::css::FlexFit).with_child(table);
+        let mut table_column = Column::new().class(pwt::css::FlexFit).with_child(
+            DataTable::new(self.columns.clone(), self.store.clone())
+                .striped(false)
+                .selection(self.selection.clone())
+                .row_render_callback(|args: &mut DataTableRowRenderArgs<RemoteTreeEntry>| {
+                    match args.record() {
+                        RemoteTreeEntry::Vnet(vnet) if vnet.external || vnet.imported => {
+                            args.add_class("pwt-opacity-50");
+                        }
+                        RemoteTreeEntry::Remote(_) => args.add_class("pwt-bg-color-surface"),
+                        _ => (),
+                    };
+                })
+                .class(css::FlexFit),
+        );
 
         if let Some(msg) = &self.error_msg {
-            column.add_child(error_message(msg.as_ref()));
+            table_column.add_child(error_message(msg.as_ref()));
         }
 
-        column.into()
+        table_column.into()
     }
 
     fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
diff --git a/ui/src/sdn/evpn/vrf_tree.rs b/ui/src/sdn/evpn/vrf_tree.rs
index 0de4145..8481dfc 100644
--- a/ui/src/sdn/evpn/vrf_tree.rs
+++ b/ui/src/sdn/evpn/vrf_tree.rs
@@ -4,7 +4,7 @@ use std::rc::Rc;
 
 use anyhow::{anyhow, Error};
 use yew::virtual_dom::{Key, VNode};
-use yew::{Component, Context, Html, Properties};
+use yew::{Callback, Component, Context, Html, Properties};
 
 use pdm_client::types::{ListController, ListVnet, ListZone};
 use pwt::css;
@@ -17,6 +17,7 @@ use pwt::widget::data_table::{
 use pwt::widget::{error_message, Column, Fa, Row};
 use pwt_macros::widget;
 
+use crate::sdn::evpn::evpn_panel::DetailPanel;
 use crate::sdn::evpn::EvpnRouteTarget;
 
 #[widget(comp=VrfTreeComponent)]
@@ -25,6 +26,7 @@ pub struct VrfTree {
     zones: Rc<Vec<ListZone>>,
     vnets: Rc<Vec<ListVnet>>,
     controllers: Rc<Vec<ListController>>,
+    on_select: Callback<Option<DetailPanel>>,
 }
 
 impl VrfTree {
@@ -32,11 +34,13 @@ impl VrfTree {
         zones: Rc<Vec<ListZone>>,
         vnets: Rc<Vec<ListVnet>>,
         controllers: Rc<Vec<ListController>>,
+        on_select: Callback<Option<DetailPanel>>,
     ) -> Self {
         yew::props!(Self {
             zones,
             vnets,
             controllers,
+            on_select,
         })
     }
 }
@@ -333,7 +337,28 @@ impl Component for VrfTreeComponent {
         let store = TreeStore::new().view_root(false);
         let columns = Self::columns(store.clone());
 
-        let selection = Selection::new();
+        let on_select = ctx.props().on_select.clone();
+        let selection_store = store.clone();
+        let selection = Selection::new().on_select(move |selection: Selection| {
+            if let Some(selected_key) = selection.selected_key() {
+                let read_guard = selection_store.read();
+
+                if let Some(node) = read_guard.lookup_node(&selected_key) {
+                    match node.record() {
+                        VrfTreeEntry::Remote(remote) => {
+                            on_select.emit(Some(DetailPanel::Vnet {
+                                remote: remote.remote.clone(),
+                                vnet: remote.vnet.clone(),
+                            }));
+                        }
+                        _ => on_select.emit(None),
+                    }
+                }
+            } else {
+                on_select.emit(None);
+            }
+        });
+
         let mut error_msg = None;
 
         match zones_to_vrf_view(
-- 
2.47.3


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


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

* [pdm-devel] [PATCH proxmox 3/3] pve-api-types: regenerate
  2025-10-21 13:50 [pdm-devel] [PATCH proxmox{, -datacenter-manager} 0/4] generate Vec's for string-lists Hannes Laimer
@ 2025-10-21 13:50 ` Hannes Laimer
  0 siblings, 0 replies; 10+ messages in thread
From: Hannes Laimer @ 2025-10-21 13:50 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 pve-api-types/src/generated/code.rs  |   2 +-
 pve-api-types/src/generated/types.rs | 335 ++++++++++++++-------------
 2 files changed, 173 insertions(+), 164 deletions(-)

diff --git a/pve-api-types/src/generated/code.rs b/pve-api-types/src/generated/code.rs
index 8b3c6696..788e4bf5 100644
--- a/pve-api-types/src/generated/code.rs
+++ b/pve-api-types/src/generated/code.rs
@@ -900,7 +900,7 @@ where
             .maybe_arg("since", &p_since)
             .maybe_arg("source", &p_source)
             .maybe_arg("start", &p_start)
-            .maybe_arg("statusfilter", &p_statusfilter)
+            .maybe_list_arg("statusfilter", &p_statusfilter)
             .maybe_arg("typefilter", &p_typefilter)
             .maybe_arg("until", &p_until)
             .maybe_arg("userfilter", &p_userfilter)
diff --git a/pve-api-types/src/generated/types.rs b/pve-api-types/src/generated/types.rs
index 98cb012b..66177998 100644
--- a/pve-api-types/src/generated/types.rs
+++ b/pve-api-types/src/generated/types.rs
@@ -1027,9 +1027,13 @@ fn test_regex_compilation_5() {
             type: String,
         },
         "isis-ifaces": {
-            format: &ApiStringFormat::Pattern(&CREATE_CONTROLLER_ISIS_IFACES_RE),
+            items: {
+                description: "List item of type pve-iface.",
+                format: &ApiStringFormat::Pattern(&CREATE_CONTROLLER_ISIS_IFACES_RE),
+                type: String,
+            },
             optional: true,
-            type: String,
+            type: Array,
         },
         "isis-net": {
             format: &ApiStringFormat::Pattern(&CREATE_CONTROLLER_ISIS_NET_RE),
@@ -1050,9 +1054,13 @@ fn test_regex_compilation_5() {
             type: String,
         },
         peers: {
-            format: &ApiStringFormat::VerifyFn(verifiers::verify_ip),
+            items: {
+                description: "List item of type ip.",
+                format: &ApiStringFormat::VerifyFn(verifiers::verify_ip),
+                type: String,
+            },
             optional: true,
-            type: String,
+            type: Array,
         },
         type: {
             type: ListControllersType,
@@ -1099,7 +1107,7 @@ pub struct CreateController {
     /// 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>,
+    pub isis_ifaces: Option<Vec<String>>,
 
     /// Network Entity title for this node in the IS-IS network.
     #[serde(default, skip_serializing_if = "Option::is_none")]
@@ -1121,7 +1129,7 @@ pub struct CreateController {
 
     /// peers address list.
     #[serde(default, skip_serializing_if = "Option::is_none")]
-    pub peers: Option<String>,
+    pub peers: Option<Vec<String>>,
 
     #[serde(rename = "type")]
     pub ty: ListControllersType,
@@ -1381,9 +1389,13 @@ fn test_regex_compilation_6() {
             type: Integer,
         },
         exitnodes: {
-            format: &ApiStringFormat::Pattern(&CREATE_ZONE_EXITNODES_RE),
+            items: {
+                description: "List item of type pve-node.",
+                format: &ApiStringFormat::Pattern(&CREATE_ZONE_EXITNODES_RE),
+                type: String,
+            },
             optional: true,
-            type: String,
+            type: Array,
         },
         "exitnodes-local-routing": {
             default: false,
@@ -1417,23 +1429,35 @@ fn test_regex_compilation_6() {
             type: Integer,
         },
         nodes: {
-            format: &ApiStringFormat::Pattern(&CREATE_ZONE_NODES_RE),
+            items: {
+                description: "List item of type pve-node.",
+                format: &ApiStringFormat::Pattern(&CREATE_ZONE_NODES_RE),
+                type: String,
+            },
             optional: true,
-            type: String,
+            type: Array,
         },
         peers: {
-            format: &ApiStringFormat::VerifyFn(verifiers::verify_ip),
+            items: {
+                description: "List item of type ip.",
+                format: &ApiStringFormat::VerifyFn(verifiers::verify_ip),
+                type: String,
+            },
             optional: true,
-            type: String,
+            type: Array,
         },
         reversedns: {
             optional: true,
             type: String,
         },
         "rt-import": {
-            format: &ApiStringFormat::VerifyFn(verifiers::verify_sdn_bgp_rt),
+            items: {
+                description: "List item of type pve-sdn-bgp-rt.",
+                format: &ApiStringFormat::VerifyFn(verifiers::verify_sdn_bgp_rt),
+                type: String,
+            },
             optional: true,
-            type: String,
+            type: Array,
         },
         tag: {
             minimum: 0,
@@ -1515,7 +1539,7 @@ pub struct CreateZone {
 
     /// List of cluster node names.
     #[serde(default, skip_serializing_if = "Option::is_none")]
-    pub exitnodes: Option<String>,
+    pub exitnodes: Option<Vec<String>>,
 
     /// Allow exitnodes to connect to EVPN guests.
     #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
@@ -1552,12 +1576,12 @@ pub struct CreateZone {
 
     /// List of cluster node names.
     #[serde(default, skip_serializing_if = "Option::is_none")]
-    pub nodes: Option<String>,
+    pub nodes: Option<Vec<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>,
+    pub peers: Option<Vec<String>>,
 
     /// reverse dns api server
     #[serde(default, skip_serializing_if = "Option::is_none")]
@@ -1566,7 +1590,7 @@ pub struct CreateZone {
     /// 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>,
+    pub rt_import: Option<Vec<String>>,
 
     /// Service-VLAN Tag (outer VLAN)
     #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u64")]
@@ -1610,101 +1634,6 @@ pub enum IsRunning {
 serde_plain::derive_display_from_serialize!(IsRunning);
 serde_plain::derive_fromstr_from_deserialize!(IsRunning);
 
-const LIST_STORAGES_CONTENT: Schema =
-    proxmox_schema::ArraySchema::new("list", &StorageContent::API_SCHEMA).schema();
-
-mod list_storages_content {
-    use serde::{Deserialize, Deserializer, Serialize, Serializer};
-
-    #[doc(hidden)]
-    pub trait Ser: Sized {
-        fn ser<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error>;
-        fn de<'de, D>(deserializer: D) -> Result<Self, D::Error>
-        where
-            D: Deserializer<'de>;
-    }
-
-    impl<T: Serialize + for<'a> Deserialize<'a>> Ser for Vec<T> {
-        fn ser<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-        where
-            S: Serializer,
-        {
-            super::stringlist::serialize(&self[..], serializer, &super::LIST_STORAGES_CONTENT)
-        }
-
-        fn de<'de, D>(deserializer: D) -> Result<Self, D::Error>
-        where
-            D: Deserializer<'de>,
-        {
-            super::stringlist::deserialize(deserializer, &super::LIST_STORAGES_CONTENT)
-        }
-    }
-
-    impl<T: Ser> Ser for Option<T> {
-        fn ser<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-        where
-            S: Serializer,
-        {
-            match self {
-                None => serializer.serialize_none(),
-                Some(inner) => inner.ser(serializer),
-            }
-        }
-
-        fn de<'de, D>(deserializer: D) -> Result<Self, D::Error>
-        where
-            D: Deserializer<'de>,
-        {
-            use std::fmt;
-            use std::marker::PhantomData;
-
-            struct V<T: Ser>(PhantomData<T>);
-
-            impl<'de, T: Ser> serde::de::Visitor<'de> for V<T> {
-                type Value = Option<T>;
-
-                fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
-                    f.write_str("an optional string")
-                }
-
-                fn visit_none<E: serde::de::Error>(self) -> Result<Self::Value, E> {
-                    Ok(None)
-                }
-
-                fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
-                where
-                    D: Deserializer<'de>,
-                {
-                    T::de(deserializer).map(Some)
-                }
-
-                fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
-                    use serde::de::IntoDeserializer;
-                    T::de(value.into_deserializer()).map(Some)
-                }
-            }
-
-            deserializer.deserialize_option(V::<T>(PhantomData))
-        }
-    }
-
-    pub fn serialize<T, S>(this: &T, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: serde::Serializer,
-        T: Ser,
-    {
-        this.ser(serializer)
-    }
-
-    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
-    where
-        D: serde::Deserializer<'de>,
-        T: Ser,
-    {
-        T::de(deserializer)
-    }
-}
-
 #[api]
 /// Only list sdn controllers of specific type
 #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
@@ -1864,9 +1793,13 @@ fn test_regex_compilation_7() {
             type: Integer,
         },
         statusfilter: {
-            format: &ApiStringFormat::Pattern(&LIST_TASKS_STATUSFILTER_RE),
+            items: {
+                description: "List item of type pve-task-status-type.",
+                format: &ApiStringFormat::Pattern(&LIST_TASKS_STATUSFILTER_RE),
+                type: String,
+            },
             optional: true,
-            type: String,
+            type: Array,
         },
         typefilter: {
             optional: true,
@@ -1916,7 +1849,7 @@ pub struct ListTasks {
 
     /// List of Task States that should be returned.
     #[serde(default, skip_serializing_if = "Option::is_none")]
-    pub statusfilter: Option<String>,
+    pub statusfilter: Option<Vec<String>>,
 
     /// Only list tasks of this type (e.g., vzstart, vzdump).
     #[serde(default, skip_serializing_if = "Option::is_none")]
@@ -3450,6 +3383,7 @@ pub struct LxcStatus {
 const_regex! {
 
 MIGRATE_LXC_TARGET_RE = r##"^(?i:[a-z0-9](?i:[a-z0-9\-]*[a-z0-9])?)$"##;
+MIGRATE_LXC_TARGET_STORAGE_RE = r##"^(?i:[a-z][a-z0-9\-_.]*[a-z0-9]):(?i:[a-z][a-z0-9\-_.]*[a-z0-9])|(?i:[a-z][a-z0-9\-_.]*[a-z0-9])|1$"##;
 
 }
 
@@ -3457,6 +3391,7 @@ MIGRATE_LXC_TARGET_RE = r##"^(?i:[a-z0-9](?i:[a-z0-9\-]*[a-z0-9])?)$"##;
 fn test_regex_compilation_12() {
     use regex::Regex;
     let _: &Regex = &MIGRATE_LXC_TARGET_RE;
+    let _: &Regex = &MIGRATE_LXC_TARGET_STORAGE_RE;
 }
 #[api(
     properties: {
@@ -3477,9 +3412,13 @@ fn test_regex_compilation_12() {
             type: String,
         },
         "target-storage": {
-            format: &ApiStringFormat::VerifyFn(verifiers::verify_storage_pair),
+            items: {
+                description: "List item of type storage-pair.",
+                format: &ApiStringFormat::Pattern(&MIGRATE_LXC_TARGET_STORAGE_RE),
+                type: String,
+            },
             optional: true,
-            type: String,
+            type: Array,
         },
         timeout: {
             default: 180,
@@ -3514,7 +3453,7 @@ pub struct MigrateLxc {
     /// '1' will map each source storage to itself.
     #[serde(default, skip_serializing_if = "Option::is_none")]
     #[serde(rename = "target-storage")]
-    pub target_storage: Option<String>,
+    pub target_storage: Option<Vec<String>>,
 
     /// Timeout in seconds for shutdown for restart migration
     #[serde(deserialize_with = "proxmox_serde::perl::deserialize_i64")]
@@ -3525,6 +3464,7 @@ pub struct MigrateLxc {
 const_regex! {
 
 MIGRATE_QEMU_TARGET_RE = r##"^(?i:[a-z0-9](?i:[a-z0-9\-]*[a-z0-9])?)$"##;
+MIGRATE_QEMU_TARGETSTORAGE_RE = r##"^(?i:[a-z][a-z0-9\-_.]*[a-z0-9]):(?i:[a-z][a-z0-9\-_.]*[a-z0-9])|(?i:[a-z][a-z0-9\-_.]*[a-z0-9])|1$"##;
 
 }
 
@@ -3532,6 +3472,7 @@ MIGRATE_QEMU_TARGET_RE = r##"^(?i:[a-z0-9](?i:[a-z0-9\-]*[a-z0-9])?)$"##;
 fn test_regex_compilation_13() {
     use regex::Regex;
     let _: &Regex = &MIGRATE_QEMU_TARGET_RE;
+    let _: &Regex = &MIGRATE_QEMU_TARGETSTORAGE_RE;
 }
 #[api(
     properties: {
@@ -3562,9 +3503,13 @@ fn test_regex_compilation_13() {
             type: String,
         },
         targetstorage: {
-            format: &ApiStringFormat::VerifyFn(verifiers::verify_storage_pair),
+            items: {
+                description: "List item of type storage-pair.",
+                format: &ApiStringFormat::Pattern(&MIGRATE_QEMU_TARGETSTORAGE_RE),
+                type: String,
+            },
             optional: true,
-            type: String,
+            type: Array,
         },
         "with-conntrack-state": {
             default: false,
@@ -3609,7 +3554,7 @@ pub struct MigrateQemu {
     /// ID maps all source storages to that storage. Providing the special value
     /// '1' will map each source storage to itself.
     #[serde(default, skip_serializing_if = "Option::is_none")]
-    pub targetstorage: Option<String>,
+    pub targetstorage: Option<Vec<String>>,
 
     /// Whether to migrate conntrack entries for running VMs.
     #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
@@ -9762,6 +9707,19 @@ pub struct ReloadSdn {
     pub release_lock: Option<bool>,
 }
 
+const_regex! {
+
+REMOTE_MIGRATE_LXC_TARGET_BRIDGE_RE = r##"^[-_.\w\d]+:[-_.\w\d]+|[-_.\w\d]+|1$"##;
+REMOTE_MIGRATE_LXC_TARGET_STORAGE_RE = r##"^(?i:[a-z][a-z0-9\-_.]*[a-z0-9]):(?i:[a-z][a-z0-9\-_.]*[a-z0-9])|(?i:[a-z][a-z0-9\-_.]*[a-z0-9])|1$"##;
+
+}
+
+#[test]
+fn test_regex_compilation_26() {
+    use regex::Regex;
+    let _: &Regex = &REMOTE_MIGRATE_LXC_TARGET_BRIDGE_RE;
+    let _: &Regex = &REMOTE_MIGRATE_LXC_TARGET_STORAGE_RE;
+}
 #[api(
     properties: {
         bwlimit: {
@@ -9781,16 +9739,24 @@ pub struct ReloadSdn {
             optional: true,
         },
         "target-bridge": {
-            format: &ApiStringFormat::VerifyFn(verifiers::verify_bridge_pair),
-            type: String,
+            items: {
+                description: "List item of type bridge-pair.",
+                format: &ApiStringFormat::Pattern(&REMOTE_MIGRATE_LXC_TARGET_BRIDGE_RE),
+                type: String,
+            },
+            type: Array,
         },
         "target-endpoint": {
             format: &ApiStringFormat::PropertyString(&ProxmoxRemote::API_SCHEMA),
             type: String,
         },
         "target-storage": {
-            format: &ApiStringFormat::VerifyFn(verifiers::verify_storage_pair),
-            type: String,
+            items: {
+                description: "List item of type storage-pair.",
+                format: &ApiStringFormat::Pattern(&REMOTE_MIGRATE_LXC_TARGET_STORAGE_RE),
+                type: String,
+            },
+            type: Array,
         },
         "target-vmid": {
             maximum: 999999999,
@@ -9834,7 +9800,7 @@ pub struct RemoteMigrateLxc {
     /// maps all source bridges to that bridge. Providing the special value '1'
     /// will map each source bridge to itself.
     #[serde(rename = "target-bridge")]
-    pub target_bridge: String,
+    pub target_bridge: Vec<String>,
 
     /// Remote target endpoint
     #[serde(rename = "target-endpoint")]
@@ -9844,7 +9810,7 @@ pub struct RemoteMigrateLxc {
     /// ID maps all source storages to that storage. Providing the special value
     /// '1' will map each source storage to itself.
     #[serde(rename = "target-storage")]
-    pub target_storage: String,
+    pub target_storage: Vec<String>,
 
     /// The (unique) ID of the VM.
     #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u32")]
@@ -9858,6 +9824,19 @@ pub struct RemoteMigrateLxc {
     pub timeout: Option<i64>,
 }
 
+const_regex! {
+
+REMOTE_MIGRATE_QEMU_TARGET_BRIDGE_RE = r##"^[-_.\w\d]+:[-_.\w\d]+|[-_.\w\d]+|1$"##;
+REMOTE_MIGRATE_QEMU_TARGET_STORAGE_RE = r##"^(?i:[a-z][a-z0-9\-_.]*[a-z0-9]):(?i:[a-z][a-z0-9\-_.]*[a-z0-9])|(?i:[a-z][a-z0-9\-_.]*[a-z0-9])|1$"##;
+
+}
+
+#[test]
+fn test_regex_compilation_27() {
+    use regex::Regex;
+    let _: &Regex = &REMOTE_MIGRATE_QEMU_TARGET_BRIDGE_RE;
+    let _: &Regex = &REMOTE_MIGRATE_QEMU_TARGET_STORAGE_RE;
+}
 #[api(
     properties: {
         bwlimit: {
@@ -9874,16 +9853,24 @@ pub struct RemoteMigrateLxc {
             optional: true,
         },
         "target-bridge": {
-            format: &ApiStringFormat::VerifyFn(verifiers::verify_bridge_pair),
-            type: String,
+            items: {
+                description: "List item of type bridge-pair.",
+                format: &ApiStringFormat::Pattern(&REMOTE_MIGRATE_QEMU_TARGET_BRIDGE_RE),
+                type: String,
+            },
+            type: Array,
         },
         "target-endpoint": {
             format: &ApiStringFormat::PropertyString(&ProxmoxRemote::API_SCHEMA),
             type: String,
         },
         "target-storage": {
-            format: &ApiStringFormat::VerifyFn(verifiers::verify_storage_pair),
-            type: String,
+            items: {
+                description: "List item of type storage-pair.",
+                format: &ApiStringFormat::Pattern(&REMOTE_MIGRATE_QEMU_TARGET_STORAGE_RE),
+                type: String,
+            },
+            type: Array,
         },
         "target-vmid": {
             maximum: 999999999,
@@ -9917,7 +9904,7 @@ pub struct RemoteMigrateQemu {
     /// maps all source bridges to that bridge. Providing the special value '1'
     /// will map each source bridge to itself.
     #[serde(rename = "target-bridge")]
-    pub target_bridge: String,
+    pub target_bridge: Vec<String>,
 
     /// Remote target endpoint
     #[serde(rename = "target-endpoint")]
@@ -9927,7 +9914,7 @@ pub struct RemoteMigrateQemu {
     /// ID maps all source storages to that storage. Providing the special value
     /// '1' will map each source storage to itself.
     #[serde(rename = "target-storage")]
-    pub target_storage: String,
+    pub target_storage: Vec<String>,
 
     /// The (unique) ID of the VM.
     #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u32")]
@@ -10162,7 +10149,7 @@ SDN_CONTROLLER_ISIS_NET_RE = r##"^[a-fA-F0-9]{2}(\.[a-fA-F0-9]{4}){3,9}\.[a-fA-F
 }
 
 #[test]
-fn test_regex_compilation_26() {
+fn test_regex_compilation_28() {
     use regex::Regex;
     let _: &Regex = &SDN_CONTROLLER_ISIS_IFACES_RE;
     let _: &Regex = &SDN_CONTROLLER_ISIS_NET_RE;
@@ -10315,7 +10302,7 @@ SDN_CONTROLLER_PENDING_ISIS_NET_RE = r##"^[a-fA-F0-9]{2}(\.[a-fA-F0-9]{4}){3,9}\
 }
 
 #[test]
-fn test_regex_compilation_27() {
+fn test_regex_compilation_29() {
     use regex::Regex;
     let _: &Regex = &SDN_CONTROLLER_PENDING_ISIS_IFACES_RE;
     let _: &Regex = &SDN_CONTROLLER_PENDING_ISIS_NET_RE;
@@ -10610,7 +10597,7 @@ SDN_ZONE_EXITNODES_PRIMARY_RE = r##"^(?i:[a-z0-9](?i:[a-z0-9\-]*[a-z0-9])?)$"##;
 }
 
 #[test]
-fn test_regex_compilation_28() {
+fn test_regex_compilation_30() {
     use regex::Regex;
     let _: &Regex = &SDN_ZONE_EXITNODES_RE;
     let _: &Regex = &SDN_ZONE_EXITNODES_PRIMARY_RE;
@@ -10884,7 +10871,7 @@ SDN_ZONE_PENDING_EXITNODES_PRIMARY_RE = r##"^(?i:[a-z0-9](?i:[a-z0-9\-]*[a-z0-9]
 }
 
 #[test]
-fn test_regex_compilation_29() {
+fn test_regex_compilation_31() {
     use regex::Regex;
     let _: &Regex = &SDN_ZONE_PENDING_EXITNODES_RE;
     let _: &Regex = &SDN_ZONE_PENDING_EXITNODES_PRIMARY_RE;
@@ -11209,13 +11196,15 @@ pub struct StartLxc {
 const_regex! {
 
 START_QEMU_MIGRATEDFROM_RE = r##"^(?i:[a-z0-9](?i:[a-z0-9\-]*[a-z0-9])?)$"##;
+START_QEMU_TARGETSTORAGE_RE = r##"^(?i:[a-z][a-z0-9\-_.]*[a-z0-9]):(?i:[a-z][a-z0-9\-_.]*[a-z0-9])|(?i:[a-z][a-z0-9\-_.]*[a-z0-9])|1$"##;
 
 }
 
 #[test]
-fn test_regex_compilation_30() {
+fn test_regex_compilation_32() {
     use regex::Regex;
     let _: &Regex = &START_QEMU_MIGRATEDFROM_RE;
+    let _: &Regex = &START_QEMU_TARGETSTORAGE_RE;
 }
 #[api(
     properties: {
@@ -11256,9 +11245,13 @@ fn test_regex_compilation_30() {
             type: String,
         },
         targetstorage: {
-            format: &ApiStringFormat::VerifyFn(verifiers::verify_storage_pair),
+            items: {
+                description: "List item of type storage-pair.",
+                format: &ApiStringFormat::Pattern(&START_QEMU_TARGETSTORAGE_RE),
+                type: String,
+            },
             optional: true,
-            type: String,
+            type: Array,
         },
         timeout: {
             default: 30,
@@ -11316,7 +11309,7 @@ pub struct StartQemu {
     /// ID maps all source storages to that storage. Providing the special value
     /// '1' will map each source storage to itself.
     #[serde(default, skip_serializing_if = "Option::is_none")]
-    pub targetstorage: Option<String>,
+    pub targetstorage: Option<Vec<String>>,
 
     /// Wait maximal timeout seconds.
     #[serde(deserialize_with = "proxmox_serde::perl::deserialize_u64")]
@@ -11379,7 +11372,7 @@ STOP_QEMU_MIGRATEDFROM_RE = r##"^(?i:[a-z0-9](?i:[a-z0-9\-]*[a-z0-9])?)$"##;
 }
 
 #[test]
-fn test_regex_compilation_31() {
+fn test_regex_compilation_33() {
     use regex::Regex;
     let _: &Regex = &STOP_QEMU_MIGRATEDFROM_RE;
 }
@@ -11478,7 +11471,7 @@ STORAGE_INFO_STORAGE_RE = r##"^(?i:[a-z][a-z0-9\-_.]*[a-z0-9])$"##;
 }
 
 #[test]
-fn test_regex_compilation_32() {
+fn test_regex_compilation_34() {
     use regex::Regex;
     let _: &Regex = &STORAGE_INFO_STORAGE_RE;
 }
@@ -11756,7 +11749,7 @@ UPDATE_QEMU_CONFIG_VMSTATESTORAGE_RE = r##"^(?i:[a-z][a-z0-9\-_.]*[a-z0-9])$"##;
 }
 
 #[test]
-fn test_regex_compilation_33() {
+fn test_regex_compilation_35() {
     use regex::Regex;
     let _: &Regex = &UPDATE_QEMU_CONFIG_AFFINITY_RE;
     let _: &Regex = &UPDATE_QEMU_CONFIG_BOOTDISK_RE;
@@ -11882,9 +11875,13 @@ fn test_regex_compilation_33() {
             type: Integer,
         },
         delete: {
-            format: &ApiStringFormat::Pattern(&UPDATE_QEMU_CONFIG_DELETE_RE),
+            items: {
+                description: "List item of type pve-configid.",
+                format: &ApiStringFormat::Pattern(&UPDATE_QEMU_CONFIG_DELETE_RE),
+                type: String,
+            },
             optional: true,
-            type: String,
+            type: Array,
         },
         description: {
             max_length: 8192,
@@ -11989,9 +11986,13 @@ fn test_regex_compilation_33() {
             type: String,
         },
         nameserver: {
-            format: &ApiStringFormat::VerifyFn(verifiers::verify_address),
+            items: {
+                description: "List item of type address.",
+                format: &ApiStringFormat::VerifyFn(verifiers::verify_address),
+                type: String,
+            },
             optional: true,
-            type: String,
+            type: Array,
         },
         net: {
             type: QemuConfigNetArray,
@@ -12023,9 +12024,13 @@ fn test_regex_compilation_33() {
             optional: true,
         },
         revert: {
-            format: &ApiStringFormat::Pattern(&UPDATE_QEMU_CONFIG_REVERT_RE),
+            items: {
+                description: "List item of type pve-configid.",
+                format: &ApiStringFormat::Pattern(&UPDATE_QEMU_CONFIG_REVERT_RE),
+                type: String,
+            },
             optional: true,
-            type: String,
+            type: Array,
         },
         rng0: {
             format: &ApiStringFormat::PropertyString(&PveQmRng::API_SCHEMA),
@@ -12104,9 +12109,13 @@ fn test_regex_compilation_33() {
             optional: true,
         },
         tags: {
-            format: &ApiStringFormat::Pattern(&UPDATE_QEMU_CONFIG_TAGS_RE),
+            items: {
+                description: "List item of type pve-tag.",
+                format: &ApiStringFormat::Pattern(&UPDATE_QEMU_CONFIG_TAGS_RE),
+                type: String,
+            },
             optional: true,
-            type: String,
+            type: Array,
         },
         tdf: {
             default: false,
@@ -12274,7 +12283,7 @@ pub struct UpdateQemuConfig {
 
     /// A list of settings you want to delete.
     #[serde(default, skip_serializing_if = "Option::is_none")]
-    pub delete: Option<String>,
+    pub delete: Option<Vec<String>>,
 
     /// Description for the VM. Shown in the web-interface VM's summary. This is
     /// saved as comment inside the configuration file.
@@ -12413,7 +12422,7 @@ pub struct UpdateQemuConfig {
     /// automatically use the setting from the host if neither searchdomain nor
     /// nameserver are set.
     #[serde(default, skip_serializing_if = "Option::is_none")]
-    pub nameserver: Option<String>,
+    pub nameserver: Option<Vec<String>>,
 
     /// Specify network devices.
     #[serde(flatten)]
@@ -12453,7 +12462,7 @@ pub struct UpdateQemuConfig {
 
     /// Revert a pending change.
     #[serde(default, skip_serializing_if = "Option::is_none")]
-    pub revert: Option<String>,
+    pub revert: Option<Vec<String>>,
 
     /// Configure a VirtIO-based Random Number Generator.
     #[serde(default, skip_serializing_if = "Option::is_none")]
@@ -12538,7 +12547,7 @@ pub struct UpdateQemuConfig {
 
     /// Tags of the VM. This is only meta information.
     #[serde(default, skip_serializing_if = "Option::is_none")]
-    pub tags: Option<String>,
+    pub tags: Option<Vec<String>>,
 
     /// Enable/disable time drift fix.
     #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
@@ -12649,7 +12658,7 @@ UPDATE_QEMU_CONFIG_EFIDISK0_SIZE_RE = r##"^(\d+(\.\d+)?)([KMGT])?$"##;
 }
 
 #[test]
-fn test_regex_compilation_34() {
+fn test_regex_compilation_36() {
     use regex::Regex;
     let _: &Regex = &UPDATE_QEMU_CONFIG_EFIDISK0_SIZE_RE;
 }
@@ -12726,7 +12735,7 @@ UPDATE_QEMU_CONFIG_IDE_SIZE_RE = r##"^(\d+(\.\d+)?)([KMGT])?$"##;
 }
 
 #[test]
-fn test_regex_compilation_35() {
+fn test_regex_compilation_37() {
     use regex::Regex;
     let _: &Regex = &UPDATE_QEMU_CONFIG_IDE_MODEL_RE;
     let _: &Regex = &UPDATE_QEMU_CONFIG_IDE_SERIAL_RE;
@@ -13082,7 +13091,7 @@ UPDATE_QEMU_CONFIG_SATA_SIZE_RE = r##"^(\d+(\.\d+)?)([KMGT])?$"##;
 }
 
 #[test]
-fn test_regex_compilation_36() {
+fn test_regex_compilation_38() {
     use regex::Regex;
     let _: &Regex = &UPDATE_QEMU_CONFIG_SATA_SERIAL_RE;
     let _: &Regex = &UPDATE_QEMU_CONFIG_SATA_SIZE_RE;
@@ -13427,7 +13436,7 @@ UPDATE_QEMU_CONFIG_SCSI_SIZE_RE = r##"^(\d+(\.\d+)?)([KMGT])?$"##;
 }
 
 #[test]
-fn test_regex_compilation_37() {
+fn test_regex_compilation_39() {
     use regex::Regex;
     let _: &Regex = &UPDATE_QEMU_CONFIG_SCSI_SERIAL_RE;
     let _: &Regex = &UPDATE_QEMU_CONFIG_SCSI_SIZE_RE;
@@ -13827,7 +13836,7 @@ UPDATE_QEMU_CONFIG_TPMSTATE0_SIZE_RE = r##"^(\d+(\.\d+)?)([KMGT])?$"##;
 }
 
 #[test]
-fn test_regex_compilation_38() {
+fn test_regex_compilation_40() {
     use regex::Regex;
     let _: &Regex = &UPDATE_QEMU_CONFIG_TPMSTATE0_SIZE_RE;
 }
@@ -13883,7 +13892,7 @@ UPDATE_QEMU_CONFIG_VIRTIO_SIZE_RE = r##"^(\d+(\.\d+)?)([KMGT])?$"##;
 }
 
 #[test]
-fn test_regex_compilation_39() {
+fn test_regex_compilation_41() {
     use regex::Regex;
     let _: &Regex = &UPDATE_QEMU_CONFIG_VIRTIO_SERIAL_RE;
     let _: &Regex = &UPDATE_QEMU_CONFIG_VIRTIO_SIZE_RE;
-- 
2.47.3



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


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

end of thread, other threads:[~2025-11-07  8:59 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-11-07  8:59 [pdm-devel] [PATCH proxmox{, -datacenter-manager} 0/8] Integration of IP-VRF and MAC-VRF status to EVPN panel Stefan Hanreich
2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox 1/3] pve-api-types: add zone / vnet status reporting endpoints Stefan Hanreich
2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox 2/3] pve-api-types: generate ip-vrf / mac-vrf endpoints Stefan Hanreich
2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox 3/3] pve-api-types: regenerate Stefan Hanreich
2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox-datacenter-manager 1/5] server: api: sdn: add ip-vrf endpoint Stefan Hanreich
2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox-datacenter-manager 2/5] server: api: sdn: add mac-vrf endpoint Stefan Hanreich
2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox-datacenter-manager 3/5] ui: sdn: evpn: add zone status panel Stefan Hanreich
2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox-datacenter-manager 4/5] ui: sdn: evpn: add vnet " Stefan Hanreich
2025-11-07  8:59 ` [pdm-devel] [PATCH proxmox-datacenter-manager 5/5] sdn: evpn: add detail panel to the evpn panel Stefan Hanreich
  -- strict thread matches above, loose matches on Subject: below --
2025-10-21 13:50 [pdm-devel] [PATCH proxmox{, -datacenter-manager} 0/4] generate Vec's for string-lists Hannes Laimer
2025-10-21 13:50 ` [pdm-devel] [PATCH proxmox 3/3] pve-api-types: regenerate Hannes Laimer

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