public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN
@ 2026-05-08 16:31 Stefan Hanreich
  2026-05-08 16:31 ` [PATCH proxmox-ve-rs v6 01/24] sdn: prefix lists: refactor section config and api format Stefan Hanreich
                   ` (23 more replies)
  0 siblings, 24 replies; 25+ messages in thread
From: Stefan Hanreich @ 2026-05-08 16:31 UTC (permalink / raw)
  To: pve-devel

Following the off-list discussion between Thomas, Dominik and me this patch
series contains the remaining pve-manager patches from the initial series and
implements changes to the API structure of route maps and prefix lists. For more
details on the exact API changes, see the respective pve-network commits that
give an overview of the new API structure, as well as provide reasoning for why
they were changed.

Handling of sequence numbers in prefix list entries has been changed as well.
They are now required in the section config, but not the API. If users do not
explicitly set a sequence number in a prefix list entry, then it will be
auto-generated in the same way as FRR auto-generates them (highest existing
sequence number + 5).

To make reviewing easier, the changes have been made on top of the existing
commits, so it is easy to see what changed from the initial UI patches.

(omitted the rest of the cover letter since it only talks about already merged
stuff)

## Changelog

Changes from v5 (Thanks @Thomas, @Dominik, @Robert, @Lukas, @Gabriel):
* overhaul API structure for both route maps / prefix lists
* adapt UI to the new API structure
* add new CRUD endpoints for prefix list entries
* expose sequence number field in the UI
* implement sequence number auto-generation
* change the prefix >=/<= fields to integerfields
* change the route map order field to an integerfield
* implement backend validation for prefix list entries
* add tests for prefix list entry validation
* fix le and ge not getting deleted when left empty on updating

Changes from v4 (Thanks @Hannes):
* Properly persist route maps / prefix lists to running config
* Consider route maps / prefix lists when rolling back
* Consider route maps / prefix lists when doing a check if FRR needs to be
  reloaded
* Fix change detection for exit action field in the UI
* Fix issue with adding a prefix list if one had already been added in the UI
* Fix exit policy rendering in route map overview
* Add default setting to exit policy dropdown
* fix error message when failing to delete prefix list
* fix check_reference using wrong property string format when deleting prefix
  lists

Changes from v3:
* added 3 commits in ve-rs that were missing due to layer-8 errors in rebasing

Changes from v2(Thanks @Wolfgang, @Gabriel, @Hannes):
* Add UI integration for prefix list / route map generation
* Add route filter based on prefix lists to openfabric / OSPF
* integrate routemap in / out parameters in BGP / EVPN controller UI
* generate route maps / prefix lists in FRR dry-run
* improve validation in the backend considerably
* add protected flag to API endpoints that require elevated privileges
* fix jinja templates for FRR config due to minijinja whitespace handling 
  changes
* refactored IntegerWithSign into ModifyNumber

Changes from v1 (Thanks @Gabriel, @Hannes, @Wolfgang):
* rebase on top of current master
* fix newly introduced vtysh tests
* include missing access-control patch
* fix an error in the permission API path of GET /route-maps/{route-map-id}
* fix permission check in list route maps / prefix lists endpoint
* implement From instead of Into for section config to frr conversions
* replace core::* imports with std::*
* improve comments in both pve-rs modules
* use get() instead of iter().find() in get methods of both pve-rs modules
* use entry API when creating new entities in both pve-rs modules
* removed duplicate PrefixList implementation block
* fixed pending parameter in GET endpoints
* add route maps / prefix lists to has_pending_changes method
* fixed change detection for newly introduced fields in prefix lists / route
  maps
* fixed reserved id 'loopbacks_ips' for prefix lists (instead of reserving
  loopback_ips)
* properly pass delete parameter to the route map update pve-rs method
* remove additional prefix list / route map rendering methods and just use dump
  instead in the ve-config FRR integration tests
* improved documentation of the FRR route map generation logic, so it better
  explains *how* the configuration gets merged.
* added another test-case for EVPN zones with a controller with custom route-map
  + exit nodes
* implement exit action and call features of route maps
* jump into user-supplied route maps instead of replacing them directly, to
  avoid breaking exit-node setups if users do not recreate the auto-generated
  route map
* improve indentation of FRR template
* update tests to reflect changes w.r.t. FRR config generation
* improve error message on trying to GET non-existing route map entry
* move the tests from the frr module in route maps / prefix lists to
  the integration tests in proxmox-ve-config
* make order u16 instead of u32, because in FRR it is an u16 as well
* add unit tests to some new types
* change route map merging logic to overwrite existing route maps, if an entry
  with the same route map name exists in the section config
* added separate patch for PrefixListName::new, since the vtysh patch from
  gabriel hasn't been applied yet, but this patch series requires the new
  function


proxmox-ve-rs:

Stefan Hanreich (2):
  sdn: prefix lists: refactor section config and api format
  prefix lists: implement validation for prefix lists

 proxmox-ve-config/src/sdn/prefix_list.rs     | 551 ++++++++++++++++++-
 proxmox-ve-config/tests/prefix_lists/main.rs |  30 +-
 2 files changed, 550 insertions(+), 31 deletions(-)


proxmox-perl-rs:

Stefan Hanreich (4):
  sdn: prefix lists: refactor existing API endpoint
  sdn: prefix lists: add crud methods for prefix list entries
  sdn: prefix lists: validate prefix lists
  sdn: route maps: add route map list method

 pve-rs/src/bindings/sdn/prefix_lists.rs | 157 ++++++++++++++++++------
 pve-rs/src/bindings/sdn/route_maps.rs   |  27 ++++
 2 files changed, 148 insertions(+), 36 deletions(-)


pve-network:

Stefan Hanreich (2):
  api: refactor route map api structure
  api: refactor prefix list api structure

 src/PVE/API2/Network/SDN/Makefile             |   1 +
 src/PVE/API2/Network/SDN/PrefixLists.pm       | 145 +++---------
 src/PVE/API2/Network/SDN/PrefixLists/Makefile |   9 +
 .../Network/SDN/PrefixLists/PrefixList.pm     | 139 ++++++++++++
 .../SDN/PrefixLists/PrefixListEntry.pm        | 208 ++++++++++++++++++
 src/PVE/API2/Network/SDN/RouteMaps.pm         |  93 ++------
 src/PVE/API2/Network/SDN/RouteMaps/Makefile   |   2 +-
 .../API2/Network/SDN/RouteMaps/RouteMap.pm    |   7 +-
 .../Network/SDN/RouteMaps/RouteMapEntries.pm  | 142 ++++++++++++
 .../Network/SDN/RouteMaps/RouteMapEntry.pm    |   9 +-
 src/PVE/Network/SDN/PrefixLists.pm            |  80 ++++---
 11 files changed, 596 insertions(+), 239 deletions(-)
 create mode 100644 src/PVE/API2/Network/SDN/PrefixLists/Makefile
 create mode 100644 src/PVE/API2/Network/SDN/PrefixLists/PrefixList.pm
 create mode 100644 src/PVE/API2/Network/SDN/PrefixLists/PrefixListEntry.pm
 create mode 100644 src/PVE/API2/Network/SDN/RouteMaps/RouteMapEntries.pm


pve-manager:

Dominik Csapak (2):
  ui: sdn: prefix list: add missing subjects
  ui: sdn: prefix list: adapt to changed api structure

Stefan Hanreich (14):
  ui: sdn: add route map selector
  ui: sdn: add prefix list selector
  ui: sdn: add panel for managing prefix lists
  ui: sdn: add panel for managing route map entries
  ui: sdn: bgp controller: allow configuring route maps
  ui: sdn: evpn controller: allow configuring route maps
  ui: sdn: openfabric: add route filter
  ui: sdn: ospf: add route filter setting
  sdn: do not fail rendering record data if pending property is missing
  ui: sdn: route maps: adapt to new route map api structure
  ui: sdn: prefix lists: route maps: use integerfields for numbers
  ui: sdn: prefix list panel: reload data on deleting prefix list entry
  ui: prefix list panel: delete empty le and get properties
  ui: prefix list entry panel: make prefix required

 www/manager6/Makefile                         |   4 +
 www/manager6/Utils.js                         |   5 +-
 www/manager6/dc/Config.js                     |  16 +
 www/manager6/sdn/PrefixListPanel.js           | 386 +++++++
 www/manager6/sdn/PrefixListSelector.js        |  30 +
 www/manager6/sdn/RouteMapPanel.js             | 977 ++++++++++++++++++
 www/manager6/sdn/RouteMapSelector.js          |  39 +
 www/manager6/sdn/controllers/BgpEdit.js       |  14 +
 www/manager6/sdn/controllers/EvpnEdit.js      |  17 +
 .../sdn/fabrics/openfabric/FabricEdit.js      |   8 +
 www/manager6/sdn/fabrics/ospf/FabricEdit.js   |   8 +
 11 files changed, 1502 insertions(+), 2 deletions(-)
 create mode 100644 www/manager6/sdn/PrefixListPanel.js
 create mode 100644 www/manager6/sdn/PrefixListSelector.js
 create mode 100644 www/manager6/sdn/RouteMapPanel.js
 create mode 100644 www/manager6/sdn/RouteMapSelector.js


Summary over all repositories:
  26 files changed, 2796 insertions(+), 308 deletions(-)

-- 
Generated by murpp 0.11.0




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

* [PATCH proxmox-ve-rs v6 01/24] sdn: prefix lists: refactor section config and api format
  2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
@ 2026-05-08 16:31 ` Stefan Hanreich
  2026-05-08 16:31 ` [PATCH proxmox-ve-rs v6 02/24] prefix lists: implement validation for prefix lists Stefan Hanreich
                   ` (22 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2026-05-08 16:31 UTC (permalink / raw)
  To: pve-devel; +Cc: Thomas Lamprecht

Sequence numbers are now required in the section config and the
backend auto-generates them when they're not contained in the user
submitted values. If an entry is missing a sequence number, then it is
assigned the highest sequence number that already exists in the
configuration plus 5 (or 5, if there is none). For this reason, split
the section config types from the API types, since they now have a
different structure. The API types are used for consuming data sent
from the API and then converted to the section config format.

The methods of the section config type now enforce those invariants by
exposing CRUD methods for manipulating its entries. They auto-generate
sequence numbers if they are missing.

Suggested-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/src/sdn/prefix_list.rs     | 305 ++++++++++++++++++-
 proxmox-ve-config/tests/prefix_lists/main.rs |  30 +-
 2 files changed, 304 insertions(+), 31 deletions(-)

diff --git a/proxmox-ve-config/src/sdn/prefix_list.rs b/proxmox-ve-config/src/sdn/prefix_list.rs
index d250a8e..0efe81d 100644
--- a/proxmox-ve-config/src/sdn/prefix_list.rs
+++ b/proxmox-ve-config/src/sdn/prefix_list.rs
@@ -20,12 +20,14 @@
 //!   entries action=deny,prefix=192.0.2.0/24,le=24
 //! ```
 
+use std::ops::{Deref, DerefMut};
+
 use const_format::concatcp;
 use serde::{Deserialize, Serialize};
 
 use proxmox_network_types::Cidr;
 use proxmox_schema::{
-    api, api_string_type, const_regex, property_string::PropertyString, ApiStringFormat, Updater,
+    api, api_string_type, const_regex, property_string::PropertyString, ApiStringFormat,
     UpdaterType,
 };
 
@@ -69,17 +71,15 @@ pub enum PrefixListAction {
         },
     }
 )]
-#[derive(Debug, Clone, Serialize, Deserialize, Updater)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 /// IP Prefix List
 ///
 /// Corresponds to the FRR IP Prefix lists, as described in its [documentation](https://docs.frrouting.org/en/latest/filter.html#ip-prefix-list)
 pub struct PrefixListSection {
-    #[updater(skip)]
-    id: PrefixListId,
+    pub(crate) id: PrefixListId,
     /// The entries in this prefix list
     #[serde(default, skip_serializing_if = "Vec::is_empty")]
-    #[updater(serde(skip_serializing_if = "Option::is_none"))]
-    pub entries: Vec<PropertyString<PrefixListEntry>>,
+    pub(crate) entries: Vec<PropertyString<PrefixListEntry>>,
 }
 
 impl PrefixListSection {
@@ -87,6 +87,191 @@ impl PrefixListSection {
     pub fn id(&self) -> &PrefixListId {
         &self.id
     }
+
+    /// Try to update this [`PrefixListSection`].
+    ///
+    /// This method fails if the given entry list is not valid.
+    pub fn try_update(
+        &mut self,
+        updater: api::PrefixListUpdater,
+        delete: Option<Vec<api::PrefixListDeletableProperties>>,
+    ) -> Result<(), anyhow::Error> {
+        let api::PrefixListUpdater { entries } = updater;
+
+        if let Some(entries) = entries {
+            self.try_set_api_entries(entries.into_iter().map(PropertyString::into_inner))?;
+        }
+
+        for deletable_property in delete.unwrap_or_default() {
+            match deletable_property {
+                api::PrefixListDeletableProperties::Entries => {
+                    self.entries = Vec::new();
+                }
+            }
+        }
+
+        Ok(())
+    }
+
+    /// Returns the value for the next sequence number that should be inserted.
+    ///
+    /// This mirrors the logic in FRR by returning the highest existing sequence number + 5.
+    pub fn next_seq_number(&self) -> u32 {
+        self.entries
+            .iter()
+            .max_by_key(|entry| entry.seq)
+            .map(|entry| entry.seq + 5)
+            .unwrap_or(5)
+    }
+
+    /// Returns the entry with sequence number `seq`.
+    pub fn entries(&self) -> impl IntoIterator<Item = &PrefixListEntry> {
+        self.entries.iter().map(Deref::deref)
+    }
+
+    /// Returns the entry with sequence number `seq`.
+    pub fn entry(&self, seq: u32) -> Option<&PrefixListEntry> {
+        self.entries
+            .iter()
+            .find(|entry| entry.seq == seq)
+            .map(Deref::deref)
+    }
+
+    /// Returns a mutable reference to the entry with sequence number `seq`.
+    pub fn entry_mut(&mut self, seq: u32) -> Option<&mut PrefixListEntry> {
+        self.entries
+            .iter_mut()
+            .find(|entry| entry.seq == seq)
+            .map(DerefMut::deref_mut)
+    }
+
+    /// Returns the position of the entry with sequence number seq.
+    pub fn entry_position(&self, seq: u32) -> Option<usize> {
+        self.entries.iter().position(|entry| entry.seq == seq)
+    }
+
+    /// Sets the entries for this prefix list.
+    pub fn try_set_api_entries(
+        &mut self,
+        entries: impl IntoIterator<Item = api::PrefixListEntry>,
+    ) -> Result<(), anyhow::Error> {
+        let old_entries = std::mem::take(&mut self.entries);
+
+        for entry in entries {
+            if let Err(error) = self.try_insert_api_entry(entry) {
+                self.entries = old_entries;
+                return Err(error);
+            }
+        }
+
+        Ok(())
+    }
+
+    /// Try to insert a [`api::PrefixListEntry`].
+    ///
+    /// This method fails if the given entry has a sequence number, that already exists in the
+    /// configuration. If no sequence number is set in the entry, then a new sequence number will be
+    /// auto-generated via [`Self::next_seq_number`].
+    pub fn try_insert_api_entry(
+        &mut self,
+        entry: api::PrefixListEntry,
+    ) -> Result<(), anyhow::Error> {
+        if let Some(seq) = entry.seq {
+            if self.entry_position(seq).is_some() {
+                anyhow::bail!("entry with sequence number {seq} already exists!");
+            }
+        }
+
+        let entry = PrefixListEntry {
+            action: entry.action,
+            prefix: entry.prefix,
+            le: entry.le,
+            ge: entry.ge,
+            seq: entry.seq.unwrap_or_else(|| self.next_seq_number()),
+        };
+
+        self.try_insert_entry(entry)
+    }
+
+    /// Try to insert an entry.
+    ///
+    /// This method fails if the sequence number from the entry already exists in the
+    /// configuration.
+    pub fn try_insert_entry(&mut self, entry: PrefixListEntry) -> Result<(), anyhow::Error> {
+        if self.entry(entry.seq).is_some() {
+            anyhow::bail!("entry with sequence number {} already exists", entry.seq);
+        }
+
+        self.entries.push(entry.into());
+        Ok(())
+    }
+
+    /// Removes the entry with the given sequence number and returns it.
+    pub fn remove_entry(&mut self, seq: u32) -> Option<PrefixListEntry> {
+        self.entry_position(seq)
+            .map(|index| self.entries.remove(index).into_inner())
+    }
+
+    /// Try to update an entry in [`PrefixListSection`].
+    ///
+    /// This method fails if the new entry has a sequence number that already exists in the
+    /// [`PrefixListSection`].
+    pub fn try_update_entry(
+        &mut self,
+        old_seq: u32,
+        updater: api::PrefixListEntryUpdater,
+        delete: Vec<api::PrefixListEntryDeletableProperties>,
+    ) -> Result<(), anyhow::Error> {
+        let api::PrefixListEntryUpdater {
+            action,
+            prefix,
+            le,
+            ge,
+            seq,
+        } = updater;
+
+        if let Some(seq) = updater.seq {
+            if seq != old_seq && self.entry(seq).is_some() {
+                anyhow::bail!("entry with sequence number {seq} already exists!");
+            }
+        }
+
+        let mut existing_entry = self.remove_entry(old_seq).ok_or_else(|| {
+            anyhow::anyhow!("entry with sequence number {old_seq} does not exist!")
+        })?;
+
+        if let Some(seq) = seq {
+            existing_entry.seq = seq;
+        }
+
+        if let Some(action) = action {
+            existing_entry.action = action;
+        }
+
+        if let Some(prefix) = prefix {
+            existing_entry.prefix = prefix;
+        }
+
+        if let Some(le) = le {
+            existing_entry.le = Some(le);
+        }
+
+        if let Some(ge) = ge {
+            existing_entry.ge = Some(ge);
+        }
+
+        for property in delete {
+            match property {
+                api::PrefixListEntryDeletableProperties::Le => existing_entry.le = None,
+                api::PrefixListEntryDeletableProperties::Ge => existing_entry.ge = None,
+                api::PrefixListEntryDeletableProperties::Seq => {
+                    existing_entry.seq = self.next_seq_number()
+                }
+            }
+        }
+
+        self.try_insert_entry(existing_entry)
+    }
 }
 
 #[api()]
@@ -106,8 +291,13 @@ pub struct PrefixListEntry {
     #[serde(skip_serializing_if = "Option::is_none")]
     ge: Option<u32>,
     /// The sequence number for this prefix list entry.
-    #[serde(skip_serializing_if = "Option::is_none")]
-    seq: Option<u32>,
+    seq: u32,
+}
+
+impl PrefixListEntry {
+    pub fn seq(&self) -> u32 {
+        self.seq
+    }
 }
 
 #[api(
@@ -121,7 +311,9 @@ pub struct PrefixListEntry {
 )]
 #[derive(Debug, Clone, Serialize, Deserialize)]
 #[serde(rename_all = "kebab-case", tag = "type")]
+/// Prefix List section config file.
 pub enum PrefixList {
+    /// A prefix list.
     PrefixList(PrefixListSection),
 }
 
@@ -150,7 +342,7 @@ pub mod frr {
                     PrefixListAction::Deny => route_map::AccessAction::Deny,
                 },
                 network: value.prefix,
-                seq: value.seq,
+                seq: Some(value.seq),
                 le: value.le,
                 ge: value.ge,
                 is_ipv6: value.prefix.is_ipv6(),
@@ -186,10 +378,62 @@ pub mod frr {
 }
 
 pub mod api {
-    use super::*;
+    use serde::{Deserialize, Serialize};
+
+    use proxmox_network_types::Cidr;
+    use proxmox_schema::{api, property_string::PropertyString, ApiStringFormat, Updater};
+
+    use super::{PrefixListAction, PrefixListId, PrefixListSection};
+
+    #[api(
+        properties: {
+            entries: {
+                type: Array,
+                optional: true,
+                items: {
+                    type: String,
+                    description: "An entry in a prefix list",
+                    format: &ApiStringFormat::PropertyString(&PrefixListEntry::API_SCHEMA),
+                }
+            },
+        }
+    )]
+    #[derive(Debug, Clone, Serialize, Deserialize, Updater)]
+    /// IP Prefix List API type.
+    ///
+    /// In the API, specifying the sequence number for entries is optional, so model that constraint here in
+    /// the API type by using the respective entry API type.
+    pub struct PrefixList {
+        #[updater(skip)]
+        pub(crate) id: PrefixListId,
+        /// The entries in this prefix list
+        #[serde(default, skip_serializing_if = "Vec::is_empty")]
+        #[updater(serde(skip_serializing_if = "Option::is_none"))]
+        pub(crate) entries: Vec<PropertyString<PrefixListEntry>>,
+    }
+
+    impl PrefixList {
+        pub fn id(&self) -> &PrefixListId {
+            &self.id
+        }
+    }
+
+    impl TryFrom<PrefixList> for PrefixListSection {
+        type Error = anyhow::Error;
 
-    pub type PrefixList = PrefixListSection;
-    pub type PrefixListUpdater = PrefixListSectionUpdater;
+        fn try_from(value: PrefixList) -> Result<Self, Self::Error> {
+            let mut section = Self {
+                id: value.id,
+                entries: Vec::new(),
+            };
+
+            for entry in value.entries {
+                section.try_insert_api_entry(entry.into_inner())?;
+            }
+
+            Ok(section)
+        }
+    }
 
     #[derive(Debug, Clone, Serialize, Deserialize)]
     #[serde(rename_all = "kebab-case")]
@@ -197,6 +441,35 @@ pub mod api {
     pub enum PrefixListDeletableProperties {
         Entries,
     }
+
+    #[api()]
+    #[derive(Debug, Clone, Serialize, Deserialize, Updater)]
+    /// IP Prefix List Entry API type.
+    ///
+    /// In the API, specifying the sequence number is optional, so model that constraint here in
+    /// the API type.
+    pub struct PrefixListEntry {
+        pub(crate) action: PrefixListAction,
+        pub(crate) prefix: Cidr,
+        /// Prefix length - entry will be applied if the prefix length is less than or equal to this
+        /// value.
+        #[serde(skip_serializing_if = "Option::is_none")]
+        pub(crate) le: Option<u32>,
+        /// Prefix length - entry will be applied if the prefix length is greater than or equal to this
+        /// value.
+        #[serde(skip_serializing_if = "Option::is_none")]
+        pub(crate) ge: Option<u32>,
+        /// The sequence number for this prefix list entry.
+        pub(crate) seq: Option<u32>,
+    }
+
+    #[derive(Debug, Clone, Serialize, Deserialize)]
+    #[serde(rename_all = "kebab-case")]
+    pub enum PrefixListEntryDeletableProperties {
+        Le,
+        Ge,
+        Seq,
+    }
 }
 
 #[cfg(test)]
@@ -209,11 +482,11 @@ mod tests {
     fn test_simple_prefix_list() -> Result<(), anyhow::Error> {
         let section_config = r#"
 prefix-list: somelist
-  entries action=permit,prefix=192.0.2.0/24
-  entries action=permit,prefix=192.0.2.0/24,le=32
+  entries action=permit,prefix=192.0.2.0/24,seq=22
+  entries action=permit,prefix=192.0.2.0/24,le=32,seq=122
   entries action=permit,prefix=192.0.2.0/24,le=32,ge=24,seq=123
-  entries action=permit,prefix=192.0.2.0/24,ge=24
-  entries action=permit,prefix=192.0.2.0/24,ge=24,le=31
+  entries action=permit,prefix=192.0.2.0/24,ge=24,seq=232
+  entries action=permit,prefix=192.0.2.0/24,ge=24,le=31,seq=222
 "#;
 
         PrefixList::parse_section_config("prefix-lists.cfg", section_config)?;
diff --git a/proxmox-ve-config/tests/prefix_lists/main.rs b/proxmox-ve-config/tests/prefix_lists/main.rs
index 2ed4894..3b12084 100644
--- a/proxmox-ve-config/tests/prefix_lists/main.rs
+++ b/proxmox-ve-config/tests/prefix_lists/main.rs
@@ -13,11 +13,11 @@ use proxmox_section_config::typed::ApiSectionDataEntry;
 fn test_build_prefix_list() -> Result<(), anyhow::Error> {
     let section_config = r#"
 prefix-list: example-1
-  entries action=permit,prefix=192.0.2.0/24
-  entries action=permit,prefix=192.0.2.0/24,le=32
+  entries action=permit,prefix=192.0.2.0/24,seq=1
+  entries action=permit,prefix=192.0.2.0/24,le=32,seq=4
   entries action=permit,prefix=192.0.2.0/24,le=32,ge=24,seq=123
-  entries action=permit,prefix=192.0.2.0/24,ge=24
-  entries action=permit,prefix=192.0.2.0/24,ge=24,le=31
+  entries action=permit,prefix=192.0.2.0/24,ge=24,seq=3
+  entries action=permit,prefix=192.0.2.0/24,ge=24,le=31,seq=2
 
 prefix-list: example-3
   entries action=permit,prefix=192.0.2.0/24,seq=333
@@ -25,8 +25,8 @@ prefix-list: example-3
   entries action=permit,prefix=203.0.113.0/24,seq=111
 
 prefix-list: example-2
-  entries action=deny,prefix=192.0.2.0/24,le=25
-  entries action=permit,prefix=192.0.2.0/24
+  entries action=deny,prefix=192.0.2.0/24,le=25,seq=111
+  entries action=permit,prefix=192.0.2.0/24,seq=121
 "#;
 
     let config = PrefixList::parse_section_config("prefix-lists.cfg", section_config)?;
@@ -42,14 +42,14 @@ prefix-list: example-2
     assert_eq!(
         dump(&frr_config)?,
         r#"!
-ip prefix-list example-1 permit 192.0.2.0/24
-ip prefix-list example-1 permit 192.0.2.0/24 le 32
+ip prefix-list example-1 seq 1 permit 192.0.2.0/24
+ip prefix-list example-1 seq 4 permit 192.0.2.0/24 le 32
 ip prefix-list example-1 seq 123 permit 192.0.2.0/24 le 32 ge 24
-ip prefix-list example-1 permit 192.0.2.0/24 ge 24
-ip prefix-list example-1 permit 192.0.2.0/24 le 31 ge 24
+ip prefix-list example-1 seq 3 permit 192.0.2.0/24 ge 24
+ip prefix-list example-1 seq 2 permit 192.0.2.0/24 le 31 ge 24
 !
-ip prefix-list example-2 deny 192.0.2.0/24 le 25
-ip prefix-list example-2 permit 192.0.2.0/24
+ip prefix-list example-2 seq 111 deny 192.0.2.0/24 le 25
+ip prefix-list example-2 seq 121 permit 192.0.2.0/24
 !
 ip prefix-list example-3 seq 333 permit 192.0.2.0/24
 ip prefix-list example-3 seq 222 permit 198.51.100.0/24
@@ -64,7 +64,7 @@ ip prefix-list example-3 seq 111 permit 203.0.113.0/24
 fn test_build_prefix_list_overwrite() -> Result<(), anyhow::Error> {
     let section_config = r#"
 prefix-list: example-1
-  entries action=permit,prefix=192.0.2.0/24
+  entries action=permit,prefix=192.0.2.0/24,seq=234
 "#;
 
     let config = PrefixList::parse_section_config("prefix-lists.cfg", section_config)?;
@@ -72,7 +72,7 @@ prefix-list: example-1
     let example_1_prefix_list = vec![FrrPrefixListRule {
         action: AccessAction::Deny,
         network: Cidr::new_v4([198, 51, 100, 0], 24).unwrap(),
-        seq: None,
+        seq: Some(234),
         le: None,
         ge: None,
         is_ipv6: false,
@@ -104,7 +104,7 @@ prefix-list: example-1
     assert_eq!(
         generated_frr_config,
         r#"!
-ip prefix-list example-1 permit 192.0.2.0/24
+ip prefix-list example-1 seq 234 permit 192.0.2.0/24
 "#
     );
 
-- 
2.47.3





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

* [PATCH proxmox-ve-rs v6 02/24] prefix lists: implement validation for prefix lists
  2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
  2026-05-08 16:31 ` [PATCH proxmox-ve-rs v6 01/24] sdn: prefix lists: refactor section config and api format Stefan Hanreich
@ 2026-05-08 16:31 ` Stefan Hanreich
  2026-05-08 16:31 ` [PATCH proxmox-perl-rs v6 03/24] sdn: prefix lists: refactor existing API endpoint Stefan Hanreich
                   ` (21 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2026-05-08 16:31 UTC (permalink / raw)
  To: pve-devel

Implement validation for prefix list entries, that enforces the
invariants that are required for entries to be valid. Since FRR
rejects invalid prefix lists and refuses to start, it is important
that invalid prefix lists are caught early by the stack to prevent
potentially crashing the FRR daemon.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 proxmox-ve-config/src/sdn/prefix_list.rs | 246 +++++++++++++++++++++++
 1 file changed, 246 insertions(+)

diff --git a/proxmox-ve-config/src/sdn/prefix_list.rs b/proxmox-ve-config/src/sdn/prefix_list.rs
index 0efe81d..672ee88 100644
--- a/proxmox-ve-config/src/sdn/prefix_list.rs
+++ b/proxmox-ve-config/src/sdn/prefix_list.rs
@@ -31,6 +31,8 @@ use proxmox_schema::{
     UpdaterType,
 };
 
+use crate::common::valid::Validatable;
+
 pub const PREFIX_LIST_ID_REGEX_STR: &str =
     r"(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-_]){0,30}(?:[a-zA-Z0-9]){0,1})";
 
@@ -82,7 +84,26 @@ pub struct PrefixListSection {
     pub(crate) entries: Vec<PropertyString<PrefixListEntry>>,
 }
 
+impl Validatable for PrefixListSection {
+    type Error = anyhow::Error;
+
+    fn validate(&self) -> Result<(), Self::Error> {
+        for entry in &self.entries {
+            entry.validate()?
+        }
+
+        Ok(())
+    }
+}
+
 impl PrefixListSection {
+    pub fn new(id: PrefixListId) -> Self {
+        Self {
+            id,
+            entries: Vec::new(),
+        }
+    }
+
     /// Return the ID of the Prefix List.
     pub fn id(&self) -> &PrefixListId {
         &self.id
@@ -202,6 +223,8 @@ impl PrefixListSection {
             anyhow::bail!("entry with sequence number {} already exists", entry.seq);
         }
 
+        entry.validate()?;
+
         self.entries.push(entry.into());
         Ok(())
     }
@@ -300,6 +323,48 @@ impl PrefixListEntry {
     }
 }
 
+impl Validatable for PrefixListEntry {
+    type Error = anyhow::Error;
+
+    fn validate(&self) -> Result<(), Self::Error> {
+        // Ensure that:
+        // prefixmask <= ge <= le
+
+        let (max_mask, current_mask) = match self.prefix {
+            Cidr::Ipv4(ipv4_cidr) => (32, ipv4_cidr.mask() as u32),
+            Cidr::Ipv6(ipv6_cidr) => (128, ipv6_cidr.mask() as u32),
+        };
+
+        if let Some(le) = self.le {
+            if le > max_mask {
+                anyhow::bail!("Prefix <= must not be greater than {max_mask}");
+            }
+
+            if current_mask > le {
+                anyhow::bail!("Prefix <= must not be greater than {current_mask}");
+            }
+
+            if let Some(ge) = self.ge {
+                if ge > le {
+                    anyhow::bail!("Prefix >= must not be greater than Prefix <= ({ge})");
+                }
+            }
+        }
+
+        if let Some(ge) = self.ge {
+            if ge > max_mask {
+                anyhow::bail!("Prefix >= must not be greater than {max_mask}");
+            }
+
+            if current_mask > ge {
+                anyhow::bail!("Prefix >= must be greater than {current_mask}");
+            }
+        }
+
+        Ok(())
+    }
+}
+
 #[api(
     "id-property": "id",
     "id-schema": {
@@ -317,6 +382,15 @@ pub enum PrefixList {
     PrefixList(PrefixListSection),
 }
 
+impl Validatable for PrefixList {
+    type Error = anyhow::Error;
+
+    fn validate(&self) -> Result<(), Self::Error> {
+        let PrefixList::PrefixList(prefix_list_section) = self;
+        prefix_list_section.validate()
+    }
+}
+
 #[cfg(feature = "frr")]
 pub mod frr {
     use super::*;
@@ -492,4 +566,176 @@ prefix-list: somelist
         PrefixList::parse_section_config("prefix-lists.cfg", section_config)?;
         Ok(())
     }
+
+    #[test]
+    fn test_prefix_list_seq_nr() -> Result<(), anyhow::Error> {
+        let mut prefix_list = PrefixListSection::new(
+            PrefixListId::from_string("test".to_string()).expect("valid prefix list id"),
+        );
+
+        assert_eq!(prefix_list.next_seq_number(), 5);
+
+        prefix_list
+            .try_insert_entry(PrefixListEntry {
+                action: PrefixListAction::Permit,
+                prefix: Cidr::new_v4([192, 0, 2, 0], 24).expect("valid cidr"),
+                le: None,
+                ge: None,
+                seq: 100,
+            })
+            .expect("valid entry");
+
+        assert_eq!(prefix_list.next_seq_number(), 105);
+
+        prefix_list.remove_entry(100).expect("could be removed");
+        assert_eq!(prefix_list.next_seq_number(), 5);
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_prefix_list_entry_update() -> Result<(), anyhow::Error> {
+        let mut prefix_list = PrefixListSection::new(
+            PrefixListId::from_string("test".to_string()).expect("valid prefix list id"),
+        );
+
+        assert_eq!(prefix_list.next_seq_number(), 5);
+
+        prefix_list
+            .try_insert_entry(PrefixListEntry {
+                action: PrefixListAction::Permit,
+                prefix: Cidr::new_v4([192, 0, 2, 0], 24).expect("valid cidr"),
+                le: None,
+                ge: None,
+                seq: 100,
+            })
+            .expect("valid entry");
+
+        prefix_list
+            .try_insert_entry(PrefixListEntry {
+                action: PrefixListAction::Permit,
+                prefix: Cidr::new_v4([192, 0, 2, 0], 24).expect("valid cidr"),
+                le: None,
+                ge: None,
+                seq: 200,
+            })
+            .expect("valid entry");
+
+        prefix_list
+            .try_update_entry(
+                100,
+                api::PrefixListEntryUpdater {
+                    action: None,
+                    prefix: None,
+                    le: None,
+                    ge: None,
+                    seq: Some(200),
+                },
+                Vec::new(),
+            )
+            .expect_err("seq nr already exists");
+
+        prefix_list
+            .try_update_entry(
+                150,
+                api::PrefixListEntryUpdater {
+                    action: None,
+                    prefix: None,
+                    le: None,
+                    ge: None,
+                    seq: Some(100),
+                },
+                Vec::new(),
+            )
+            .expect_err("old seq nr doesn't exist");
+
+        prefix_list
+            .try_update_entry(
+                100,
+                api::PrefixListEntryUpdater {
+                    action: None,
+                    prefix: None,
+                    le: None,
+                    ge: None,
+                    seq: Some(10),
+                },
+                Vec::new(),
+            )
+            .expect("changing sequence number from 100 to 10 works");
+
+        prefix_list
+            .entry(10)
+            .expect("entry has been successfully updated");
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_invalid_prefix_list_entry() -> Result<(), anyhow::Error> {
+        let mut prefix_list = PrefixListSection::new(
+            PrefixListId::from_string("test".to_string()).expect("valid prefix list id"),
+        );
+
+        prefix_list
+            .try_insert_entry(PrefixListEntry {
+                action: PrefixListAction::Permit,
+                prefix: Cidr::new_v4([192, 0, 2, 0], 24).expect("valid cidr"),
+                le: Some(23),
+                ge: None,
+                seq: 100,
+            })
+            .expect_err("le is larger than prefix size");
+
+        prefix_list
+            .try_insert_entry(PrefixListEntry {
+                action: PrefixListAction::Permit,
+                prefix: Cidr::new_v4([192, 0, 2, 0], 24).expect("valid cidr"),
+                le: Some(23),
+                ge: None,
+                seq: 100,
+            })
+            .expect_err("le is larger than prefix size");
+
+        prefix_list
+            .try_insert_entry(PrefixListEntry {
+                action: PrefixListAction::Permit,
+                prefix: Cidr::new_v4([192, 0, 2, 0], 24).expect("valid cidr"),
+                le: None,
+                ge: Some(23),
+                seq: 100,
+            })
+            .expect_err("ge is larger than prefix size");
+
+        prefix_list
+            .try_insert_entry(PrefixListEntry {
+                action: PrefixListAction::Permit,
+                prefix: Cidr::new_v4([192, 0, 2, 0], 24).expect("valid cidr"),
+                le: Some(25),
+                ge: Some(27),
+                seq: 100,
+            })
+            .expect_err("le is larger than ge");
+
+        prefix_list
+            .try_insert_entry(PrefixListEntry {
+                action: PrefixListAction::Permit,
+                prefix: Cidr::new_v4([192, 0, 2, 0], 24).expect("valid cidr"),
+                le: None,
+                ge: None,
+                seq: 100,
+            })
+            .expect("valid entry");
+
+        prefix_list
+            .try_insert_entry(PrefixListEntry {
+                action: PrefixListAction::Permit,
+                prefix: Cidr::new_v4([192, 0, 2, 0], 24).expect("valid cidr"),
+                le: None,
+                ge: None,
+                seq: 100,
+            })
+            .expect_err("entry with seq already exists");
+
+        Ok(())
+    }
 }
-- 
2.47.3





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

* [PATCH proxmox-perl-rs v6 03/24] sdn: prefix lists: refactor existing API endpoint
  2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
  2026-05-08 16:31 ` [PATCH proxmox-ve-rs v6 01/24] sdn: prefix lists: refactor section config and api format Stefan Hanreich
  2026-05-08 16:31 ` [PATCH proxmox-ve-rs v6 02/24] prefix lists: implement validation for prefix lists Stefan Hanreich
@ 2026-05-08 16:31 ` Stefan Hanreich
  2026-05-08 16:31 ` [PATCH proxmox-perl-rs v6 04/24] sdn: prefix lists: add crud methods for prefix list entries Stefan Hanreich
                   ` (20 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2026-05-08 16:31 UTC (permalink / raw)
  To: pve-devel

proxmox-ve-config now uses dedicated API types for the prefix list
entries. This was necessary, because the sequence number in the prefix
list entries is now required in the section config - but not when
submitting values via the API. The existing API type for prefix lists,
now uses the prefix list entry API type as well, to handle this
change.

List and get endpoints will always return the config type, since they
return the values from the configuration - while the create and update
endpoints will utilize the API types, since they contain the proper
format for values submitted by users. Additionally, change the return
type of the list and get endpoints since they're infallible.

The updater method has been moved into proxmox-ve-config. This allows
for leaving the field private, easier validation and automatic
numbering of entries.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 pve-rs/src/bindings/sdn/prefix_lists.rs | 57 +++++++++----------------
 1 file changed, 19 insertions(+), 38 deletions(-)

diff --git a/pve-rs/src/bindings/sdn/prefix_lists.rs b/pve-rs/src/bindings/sdn/prefix_lists.rs
index eafb70d..4ca6999 100644
--- a/pve-rs/src/bindings/sdn/prefix_lists.rs
+++ b/pve-rs/src/bindings/sdn/prefix_lists.rs
@@ -18,9 +18,13 @@ pub mod pve_rs_sdn_prefix_lists {
     use perlmod::Value;
     use proxmox_section_config::typed::{ApiSectionDataEntry, SectionConfigData};
     use proxmox_ve_config::sdn::prefix_list::api::{
-        PrefixList as ApiPrefixList, PrefixListDeletableProperties, PrefixListUpdater,
+        PrefixList as ApiPrefixList, PrefixListDeletableProperties,
+        PrefixListEntry as ApiPrefixListEntry, PrefixListEntryDeletableProperties,
+        PrefixListEntryUpdater, PrefixListUpdater,
+    };
+    use proxmox_ve_config::sdn::prefix_list::{
+        PrefixList as ConfigPrefixList, PrefixListEntry as ConfigPrefixListEntry, PrefixListId,
     };
-    use proxmox_ve_config::sdn::prefix_list::{PrefixList as ConfigPrefixList, PrefixListId};
 
     /// A SDN PrefixList config instance.
     #[derive(Serialize, Deserialize)]
@@ -93,19 +97,13 @@ pub mod pve_rs_sdn_prefix_lists {
 
     /// Method: Returns all prefix lists as a hash indexed with the IDs of the prefix lists.
     #[export]
-    pub fn list(
-        #[try_from_ref] this: &PerlPrefixListConfig,
-    ) -> Result<HashMap<String, ApiPrefixList>, Error> {
-        Ok(this
-            .prefix_lists
+    pub fn list(#[try_from_ref] this: &PerlPrefixListConfig) -> HashMap<String, ConfigPrefixList> {
+        this.prefix_lists
             .lock()
             .unwrap()
             .iter()
-            .map(|(id, prefix_list)| {
-                let ConfigPrefixList::PrefixList(prefix_list) = prefix_list;
-                (id.clone(), prefix_list.clone())
-            })
-            .collect())
+            .map(|(id, prefix_list)| (id.clone(), prefix_list.clone()))
+            .collect()
     }
 
     /// Method: Create a new PrefixList.
@@ -121,7 +119,9 @@ pub mod pve_rs_sdn_prefix_lists {
                 "prefix list already exists in configuration: {}",
                 prefix_list.id()
             ),
-            Entry::Vacant(vacancy) => vacancy.insert(ConfigPrefixList::PrefixList(prefix_list)),
+            Entry::Vacant(vacancy) => {
+                vacancy.insert(ConfigPrefixList::PrefixList(prefix_list.try_into()?))
+            }
         };
 
         Ok(())
@@ -132,16 +132,12 @@ pub mod pve_rs_sdn_prefix_lists {
     pub fn get(
         #[try_from_ref] this: &PerlPrefixListConfig,
         id: PrefixListId,
-    ) -> Result<Option<ApiPrefixList>, Error> {
-        Ok(this
-            .prefix_lists
+    ) -> Option<ConfigPrefixList> {
+        this.prefix_lists
             .lock()
             .unwrap()
             .get(&id.to_string())
-            .map(|prefix_list| {
-                let ConfigPrefixList::PrefixList(prefix_list) = prefix_list;
-                prefix_list.clone()
-            }))
+            .cloned()
     }
 
     /// Method: Update a PrefixList.
@@ -158,21 +154,7 @@ pub mod pve_rs_sdn_prefix_lists {
             .get_mut(id.as_str())
             .ok_or_else(|| anyhow!("Could not find prefix list with id: {}", id))?;
 
-        let PrefixListUpdater { entries } = updater;
-
-        if let Some(entries) = entries {
-            prefix_list.entries = entries;
-        }
-
-        for deletable_property in delete.unwrap_or_default() {
-            match deletable_property {
-                PrefixListDeletableProperties::Entries => {
-                    prefix_list.entries = Vec::new();
-                }
-            }
-        }
-
-        Ok(())
+        prefix_list.try_update(updater, delete)
     }
 
     /// Method: Delete a PrefixList.
@@ -185,8 +167,7 @@ pub mod pve_rs_sdn_prefix_lists {
             .lock()
             .unwrap()
             .remove(&id.to_string())
-            .ok_or_else(|| anyhow!("could not find prefix list with id: {id}"))?;
-
-        Ok(())
+            .map(|_| ())
+            .ok_or_else(|| anyhow!("could not find prefix list with id: {id}"))
     }
 }
-- 
2.47.3





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

* [PATCH proxmox-perl-rs v6 04/24] sdn: prefix lists: add crud methods for prefix list entries
  2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
                   ` (2 preceding siblings ...)
  2026-05-08 16:31 ` [PATCH proxmox-perl-rs v6 03/24] sdn: prefix lists: refactor existing API endpoint Stefan Hanreich
@ 2026-05-08 16:31 ` Stefan Hanreich
  2026-05-08 16:31 ` [PATCH proxmox-perl-rs v6 05/24] sdn: prefix lists: validate prefix lists Stefan Hanreich
                   ` (19 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2026-05-08 16:31 UTC (permalink / raw)
  To: pve-devel

Prefix list entries get exposed via their own subfolder under the
/prefix-lists folder - so they can be modified easier via the UI. The
respective logic for the API handlers is implemented directly in
ve-config, which enforces the invariants and sequence number
auto-generation.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 pve-rs/src/bindings/sdn/prefix_lists.rs | 86 +++++++++++++++++++++++++
 1 file changed, 86 insertions(+)

diff --git a/pve-rs/src/bindings/sdn/prefix_lists.rs b/pve-rs/src/bindings/sdn/prefix_lists.rs
index 4ca6999..8087a9e 100644
--- a/pve-rs/src/bindings/sdn/prefix_lists.rs
+++ b/pve-rs/src/bindings/sdn/prefix_lists.rs
@@ -170,4 +170,90 @@ pub mod pve_rs_sdn_prefix_lists {
             .map(|_| ())
             .ok_or_else(|| anyhow!("could not find prefix list with id: {id}"))
     }
+
+    /// Method: Get a specific prefix list entry.
+    #[export]
+    pub fn list_entries(
+        #[try_from_ref] this: &PerlPrefixListConfig,
+        id: PrefixListId,
+    ) -> Result<Vec<ConfigPrefixListEntry>, Error> {
+        let prefix_lists = this.prefix_lists.lock().unwrap();
+
+        let ConfigPrefixList::PrefixList(prefix_list) = prefix_lists
+            .get(&id.to_string())
+            .ok_or_else(|| anyhow!("could not find prefix list with id: {id}"))?;
+
+        Ok(prefix_list.entries().into_iter().cloned().collect())
+    }
+
+    /// Method: Get a specific prefix list entry.
+    #[export]
+    pub fn get_entry(
+        #[try_from_ref] this: &PerlPrefixListConfig,
+        id: PrefixListId,
+        seq: u32,
+    ) -> Option<ConfigPrefixListEntry> {
+        this.prefix_lists
+            .lock()
+            .unwrap()
+            .get(&id.to_string())
+            .and_then(|prefix_list| {
+                let ConfigPrefixList::PrefixList(prefix_list) = prefix_list;
+                prefix_list.entry(seq)
+            })
+            .cloned()
+    }
+
+    /// Method: Get a specific prefix list entry.
+    #[export]
+    pub fn create_entry(
+        #[try_from_ref] this: &PerlPrefixListConfig,
+        id: PrefixListId,
+        entry: ApiPrefixListEntry,
+    ) -> Result<(), Error> {
+        let mut prefix_lists = this.prefix_lists.lock().unwrap();
+
+        let ConfigPrefixList::PrefixList(prefix_list) = prefix_lists
+            .get_mut(&id.to_string())
+            .ok_or_else(|| anyhow::anyhow!("could not find prefix list with id {id}"))?;
+
+        prefix_list.try_insert_api_entry(entry)
+    }
+
+    /// Method: Get a specific prefix list entry.
+    #[export]
+    pub fn update_entry(
+        #[try_from_ref] this: &PerlPrefixListConfig,
+        id: PrefixListId,
+        seq: u32,
+        updater: PrefixListEntryUpdater,
+        delete: Option<Vec<PrefixListEntryDeletableProperties>>,
+    ) -> Result<(), Error> {
+        let mut prefix_lists = this.prefix_lists.lock().unwrap();
+
+        let ConfigPrefixList::PrefixList(prefix_list) = prefix_lists
+            .get_mut(&id.to_string())
+            .ok_or_else(|| anyhow::anyhow!("could not find prefix list with id {id}"))?;
+
+        prefix_list.try_update_entry(seq, updater, delete.unwrap_or_default())
+    }
+
+    /// Method: Remove a specific prefix list entry.
+    #[export]
+    pub fn delete_entry(
+        #[try_from_ref] this: &PerlPrefixListConfig,
+        id: PrefixListId,
+        seq: u32,
+    ) -> Result<(), Error> {
+        this.prefix_lists
+            .lock()
+            .unwrap()
+            .get_mut(&id.to_string())
+            .and_then(|prefix_list| {
+                let ConfigPrefixList::PrefixList(prefix_list) = prefix_list;
+                prefix_list.remove_entry(seq)
+            })
+            .map(|_| ())
+            .ok_or_else(|| anyhow::anyhow!("could not find prefix list entry with seq {seq}"))
+    }
 }
-- 
2.47.3





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

* [PATCH proxmox-perl-rs v6 05/24] sdn: prefix lists: validate prefix lists
  2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
                   ` (3 preceding siblings ...)
  2026-05-08 16:31 ` [PATCH proxmox-perl-rs v6 04/24] sdn: prefix lists: add crud methods for prefix list entries Stefan Hanreich
@ 2026-05-08 16:31 ` Stefan Hanreich
  2026-05-08 16:31 ` [PATCH proxmox-perl-rs v6 06/24] sdn: route maps: add route map list method Stefan Hanreich
                   ` (18 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2026-05-08 16:31 UTC (permalink / raw)
  To: pve-devel

Use the newly supplied validation methods from the prefix list section
config to validate the prefix list configuration when reading or
writing.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 pve-rs/src/bindings/sdn/prefix_lists.rs | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/pve-rs/src/bindings/sdn/prefix_lists.rs b/pve-rs/src/bindings/sdn/prefix_lists.rs
index 8087a9e..cf900c3 100644
--- a/pve-rs/src/bindings/sdn/prefix_lists.rs
+++ b/pve-rs/src/bindings/sdn/prefix_lists.rs
@@ -17,6 +17,7 @@ pub mod pve_rs_sdn_prefix_lists {
 
     use perlmod::Value;
     use proxmox_section_config::typed::{ApiSectionDataEntry, SectionConfigData};
+    use proxmox_ve_config::common::valid::Validatable;
     use proxmox_ve_config::sdn::prefix_list::api::{
         PrefixList as ApiPrefixList, PrefixListDeletableProperties,
         PrefixListEntry as ApiPrefixListEntry, PrefixListEntryDeletableProperties,
@@ -41,6 +42,10 @@ pub mod pve_rs_sdn_prefix_lists {
         let raw_config = std::str::from_utf8(raw_config)?;
         let config = ConfigPrefixList::parse_section_config("prefix-lists.cfg", raw_config)?;
 
+        for prefix_list in config.values() {
+            prefix_list.validate()?;
+        }
+
         Ok(
             perlmod::instantiate_magic!(&class, MAGIC => Box::new(PerlPrefixListConfig {
                 prefix_lists: Mutex::new(config.deref().clone()),
@@ -57,6 +62,10 @@ pub mod pve_rs_sdn_prefix_lists {
         let prefix_lists: SectionConfigData<ConfigPrefixList> =
             SectionConfigData::from_iter(prefix_lists);
 
+        for prefix_list in prefix_lists.values() {
+            prefix_list.validate()?;
+        }
+
         Ok(
             perlmod::instantiate_magic!(&class, MAGIC => Box::new(PerlPrefixListConfig {
                 prefix_lists: Mutex::new(prefix_lists.deref().clone()),
@@ -70,6 +79,11 @@ pub mod pve_rs_sdn_prefix_lists {
         #[try_from_ref] this: &PerlPrefixListConfig,
     ) -> Result<HashMap<String, ConfigPrefixList>, Error> {
         let config = this.prefix_lists.lock().unwrap();
+
+        for prefix_list in config.values() {
+            prefix_list.validate()?;
+        }
+
         Ok(config.deref().clone())
     }
 
@@ -80,6 +94,10 @@ pub mod pve_rs_sdn_prefix_lists {
     pub fn to_raw(#[try_from_ref] this: &PerlPrefixListConfig) -> Result<String, Error> {
         let config = this.prefix_lists.lock().unwrap();
 
+        for prefix_list in config.values() {
+            prefix_list.validate()?;
+        }
+
         let prefix_lists: SectionConfigData<ConfigPrefixList> =
             SectionConfigData::from_iter(config.deref().clone());
 
-- 
2.47.3





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

* [PATCH proxmox-perl-rs v6 06/24] sdn: route maps: add route map list method
  2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
                   ` (4 preceding siblings ...)
  2026-05-08 16:31 ` [PATCH proxmox-perl-rs v6 05/24] sdn: prefix lists: validate prefix lists Stefan Hanreich
@ 2026-05-08 16:31 ` Stefan Hanreich
  2026-05-08 16:31 ` [PATCH pve-network v6 07/24] api: refactor route map api structure Stefan Hanreich
                   ` (17 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2026-05-08 16:31 UTC (permalink / raw)
  To: pve-devel

Exposes a new method that returns a list of all existing route maps.
Used for implementing a new endpoint in PVE that only returns the IDs
of route maps, which is used in the route map selector component.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 pve-rs/src/bindings/sdn/route_maps.rs | 27 +++++++++++++++++++++++++++
 1 file changed, 27 insertions(+)

diff --git a/pve-rs/src/bindings/sdn/route_maps.rs b/pve-rs/src/bindings/sdn/route_maps.rs
index efac7e1..73eb271 100644
--- a/pve-rs/src/bindings/sdn/route_maps.rs
+++ b/pve-rs/src/bindings/sdn/route_maps.rs
@@ -4,6 +4,7 @@ pub mod pve_rs_sdn_route_maps {
 
     use std::collections::hash_map::Entry;
     use std::collections::HashMap;
+    use std::collections::HashSet;
     use std::ops::Deref;
     use std::sync::Mutex;
 
@@ -87,6 +88,32 @@ pub mod pve_rs_sdn_route_maps {
         Ok(hex::encode(hash))
     }
 
+    #[derive(Clone, Serialize, Deserialize, Hash)]
+    pub(crate) struct RouteMap {
+        id: RouteMapId,
+    }
+
+    /// Method: Returns all route maps.
+    #[export]
+    pub fn list_route_maps(
+        #[try_from_ref] this: &PerlRouteMapConfig,
+    ) -> Result<Vec<RouteMap>, Error> {
+        let route_maps = this.route_maps.lock().unwrap();
+
+        let route_map_ids: HashSet<&RouteMapId> = route_maps
+            .iter()
+            .map(|(_id, route_map_entry)| {
+                let ConfigRouteMap::RouteMapEntry(route_map) = route_map_entry;
+                route_map.id().route_map_id()
+            })
+            .collect();
+
+        Ok(route_map_ids
+            .into_iter()
+            .map(|id| RouteMap { id: id.clone() })
+            .collect())
+    }
+
     /// Method: Returns all route map entries as a hash indexed with the IDs of the entries.
     #[export]
     pub fn list(
-- 
2.47.3





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

* [PATCH pve-network v6 07/24] api: refactor route map api structure
  2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
                   ` (5 preceding siblings ...)
  2026-05-08 16:31 ` [PATCH proxmox-perl-rs v6 06/24] sdn: route maps: add route map list method Stefan Hanreich
@ 2026-05-08 16:31 ` Stefan Hanreich
  2026-05-08 16:31 ` [PATCH pve-network v6 08/24] api: refactor prefix list " Stefan Hanreich
                   ` (16 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2026-05-08 16:31 UTC (permalink / raw)
  To: pve-devel

Reworks the existing rotue map API structure to look as follows:

GET /route-maps
Returns a list of all route maps that exist in the configuration, the
returned data only contains the ID of the route maps. It does not
provide the usual pending parameter, since different entries could
have different pending states. This could potentially be added in the
future by looking at the pending state of all entries, and treating
any pending state of any entry as changed state for the route map. For
now this hasn't been implemented, since the endpoint is only used by
the route map selector component, which doesn't require knowledge
about the pending state of route maps.

GET /route-maps/entries
Returns a list of all entries of all route maps.

POST /route-maps/entries
Creates a new route map entry.

GET /route-maps/entries/{route-map-id}
Returns a list of all entries of a specific route map.

GET/PUT/DELETE /route-maps/entries/{route-map-id}/entry/{order}
Returns / updates / deletes a specific route map entry.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 src/PVE/API2/Network/SDN/RouteMaps.pm         |  93 ++----------
 src/PVE/API2/Network/SDN/RouteMaps/Makefile   |   2 +-
 .../API2/Network/SDN/RouteMaps/RouteMap.pm    |   7 +-
 .../Network/SDN/RouteMaps/RouteMapEntries.pm  | 142 ++++++++++++++++++
 .../Network/SDN/RouteMaps/RouteMapEntry.pm    |   9 +-
 5 files changed, 164 insertions(+), 89 deletions(-)
 create mode 100644 src/PVE/API2/Network/SDN/RouteMaps/RouteMapEntries.pm

diff --git a/src/PVE/API2/Network/SDN/RouteMaps.pm b/src/PVE/API2/Network/SDN/RouteMaps.pm
index 8a7936d9..4baffe31 100644
--- a/src/PVE/API2/Network/SDN/RouteMaps.pm
+++ b/src/PVE/API2/Network/SDN/RouteMaps.pm
@@ -3,7 +3,7 @@ package PVE::API2::Network::SDN::RouteMaps;
 use strict;
 use warnings;
 
-use PVE::API2::Network::SDN::RouteMaps::RouteMap;
+use PVE::API2::Network::SDN::RouteMaps::RouteMapEntries;
 use PVE::Exception qw(raise_param_exc);
 use PVE::JSONSchema qw(get_standard_option);
 use PVE::Network::SDN::RouteMaps;
@@ -13,8 +13,8 @@ use PVE::RESTHandler;
 use base qw(PVE::RESTHandler);
 
 __PACKAGE__->register_method({
-    subclass => "PVE::API2::Network::SDN::RouteMaps::RouteMap",
-    path => '{route-map-id}',
+    subclass => "PVE::API2::Network::SDN::RouteMaps::RouteMapEntries",
+    path => 'entries',
 });
 
 __PACKAGE__->register_method({
@@ -23,7 +23,7 @@ __PACKAGE__->register_method({
     method => 'GET',
     permissions => {
         description =>
-            "Only returns route map entries where you have 'SDN.Audit' or 'SDN.Allocate' permissions.",
+            "Only returns route maps where you have 'SDN.Audit' or 'SDN.Allocate' permissions.",
         user => 'all',
     },
     description => "List Route Maps",
@@ -34,108 +34,41 @@ __PACKAGE__->register_method({
                 optional => 1,
                 description => "Display running config.",
             },
-            pending => {
-                type => 'boolean',
-                optional => 1,
-                description => "Display pending config.",
-            },
         },
     },
     returns => {
         type => 'array',
         items => {
             type => "object",
-            properties => PVE::Network::SDN::RouteMaps::route_map_properties(0),
+            properties => {
+                id => get_standard_option('pve-sdn-route-map-id'),
+            }
         },
-        links => [{ rel => 'child', href => "{route-map-id}" }],
+        links => [{ rel => 'child', href => "entries/{id}" }],
     },
     code => sub {
         my ($param) = @_;
 
-        my $pending = extract_param($param, 'pending');
         my $running = extract_param($param, 'running');
-
-        my $digest;
-        my $route_maps;
-
-        if ($pending) {
-            my $current_config = PVE::Network::SDN::RouteMaps::config();
-            my $running_config = PVE::Network::SDN::RouteMaps::config(1);
-
-            my $pending_route_maps = PVE::Network::SDN::pending_config(
-                { 'route-maps' => { ids => $running_config->list() } },
-                { ids => $current_config->list() },
-                'route-maps',
-            );
-
-            $digest = $current_config->digest();
-            $route_maps = $pending_route_maps->{ids};
-        } elsif ($running) {
-            $route_maps = PVE::Network::SDN::RouteMaps::config(1)->list();
-        } else {
-            my $current_config = PVE::Network::SDN::RouteMaps::config();
-
-            $digest = $current_config->digest();
-            $route_maps = $current_config->list();
-        }
+        my $route_maps = PVE::Network::SDN::RouteMaps::config($running)->list_route_maps();
 
         my $rpcenv = PVE::RPCEnvironment::get();
         my $authuser = $rpcenv->get_user();
         my $route_map_privs = ['SDN.Audit', 'SDN.Allocate'];
 
         my @res;
-        for my $route_map_id (sort keys $route_maps->%*) {
+        for my $route_map ($route_maps->@*) {
             next
-                if !$rpcenv->check_any($authuser, "/sdn/route-maps/$route_map_id",
+                if !$rpcenv->check_any($authuser, "/sdn/route-maps/$route_map->{id}",
                     $route_map_privs, 1);
-            $route_maps->{$route_map_id}->{digest} = $digest if $digest;
-            push @res, $route_maps->{$route_map_id};
+
+            push @res, $route_map;
         }
 
         return \@res;
     },
 });
 
-__PACKAGE__->register_method({
-    name => 'create_route_map_entry',
-    path => '',
-    method => 'POST',
-    protected => 1,
-    permissions => {
-        check => ['perm', '/sdn/route-maps', ['SDN.Allocate']],
-    },
-    description => "Create Route Map entry",
-    parameters => {
-        properties => {
-            digest => get_standard_option('pve-config-digest'),
-            'lock-token' => get_standard_option('pve-sdn-lock-token'),
-            PVE::Network::SDN::RouteMaps::route_map_properties(0)->%*,
-        },
-    },
-    returns => {
-        type => "null",
-    },
-    code => sub {
-        my ($param) = @_;
 
-        my $lock_token = extract_param($param, 'lock-token');
-
-        PVE::Network::SDN::lock_sdn_config(
-            sub {
-                my $config = PVE::Network::SDN::RouteMaps::config();
-
-                my $digest = extract_param($param, 'digest');
-                PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
-
-                $config->create($param);
-                PVE::Network::SDN::RouteMaps::write_config($config);
-            },
-            "creating route map entry failed",
-            $lock_token,
-        );
-
-        return;
-    },
-});
 
 1;
diff --git a/src/PVE/API2/Network/SDN/RouteMaps/Makefile b/src/PVE/API2/Network/SDN/RouteMaps/Makefile
index 07b45e91..46ed800f 100644
--- a/src/PVE/API2/Network/SDN/RouteMaps/Makefile
+++ b/src/PVE/API2/Network/SDN/RouteMaps/Makefile
@@ -1,7 +1,7 @@
 SOURCES=RouteMap.pm\
+		RouteMapEntries.pm\
 		RouteMapEntry.pm
 
-
 PERL5DIR=${DESTDIR}/usr/share/perl5
 
 .PHONY: install
diff --git a/src/PVE/API2/Network/SDN/RouteMaps/RouteMap.pm b/src/PVE/API2/Network/SDN/RouteMaps/RouteMap.pm
index 3b027a0b..7565c377 100644
--- a/src/PVE/API2/Network/SDN/RouteMaps/RouteMap.pm
+++ b/src/PVE/API2/Network/SDN/RouteMaps/RouteMap.pm
@@ -13,11 +13,11 @@ use base qw(PVE::RESTHandler);
 
 __PACKAGE__->register_method({
     subclass => "PVE::API2::Network::SDN::RouteMaps::RouteMapEntry",
-    path => '{order}',
+    path => 'entry',
 });
 
 __PACKAGE__->register_method({
-    name => 'list_route_map_entries',
+    name => 'list_route_map_entries_for_route_map',
     path => '',
     method => 'GET',
     permissions => {
@@ -46,13 +46,14 @@ __PACKAGE__->register_method({
             type => "object",
             properties => PVE::Network::SDN::RouteMaps::route_map_properties(0),
         },
-        links => [{ rel => 'child', href => "{order}" }],
+        links => [{ rel => 'child', href => "entry/{route-map-id}" }],
     },
     code => sub {
         my ($param) = @_;
 
         my $pending = extract_param($param, 'pending');
         my $running = extract_param($param, 'running');
+
         my $route_map_id = extract_param($param, 'route-map-id');
 
         my $digest;
diff --git a/src/PVE/API2/Network/SDN/RouteMaps/RouteMapEntries.pm b/src/PVE/API2/Network/SDN/RouteMaps/RouteMapEntries.pm
new file mode 100644
index 00000000..220e5a7b
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/RouteMaps/RouteMapEntries.pm
@@ -0,0 +1,142 @@
+package PVE::API2::Network::SDN::RouteMaps::RouteMapEntries;
+
+use strict;
+use warnings;
+
+use PVE::API2::Network::SDN::RouteMaps::RouteMap;
+use PVE::Exception qw(raise_param_exc);
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::Network::SDN::RouteMaps;
+use PVE::Tools qw(extract_param);
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method({
+    subclass => "PVE::API2::Network::SDN::RouteMaps::RouteMap",
+    path => '{route-map-id}',
+});
+
+__PACKAGE__->register_method({
+    name => 'list_route_map_entries',
+    path => '',
+    method => 'GET',
+    permissions => {
+        description =>
+            "Only returns route map entries where you have 'SDN.Audit' or 'SDN.Allocate' permissions.",
+        user => 'all',
+    },
+    description => "Lists all route map entries.",
+    parameters => {
+        properties => {
+            running => {
+                type => 'boolean',
+                optional => 1,
+                description => "Display running config.",
+            },
+            pending => {
+                type => 'boolean',
+                optional => 1,
+                description => "Display pending config.",
+            },
+        },
+    },
+    returns => {
+        type => 'array',
+        items => {
+            type => "object",
+            properties => PVE::Network::SDN::RouteMaps::route_map_properties(0),
+        },
+        links => [{ rel => 'child', href => "{route-map-id}" }],
+    },
+    code => sub {
+        my ($param) = @_;
+
+        my $pending = extract_param($param, 'pending');
+        my $running = extract_param($param, 'running');
+
+        my $digest;
+        my $route_maps;
+
+        if ($pending) {
+            my $current_config = PVE::Network::SDN::RouteMaps::config();
+            my $running_config = PVE::Network::SDN::RouteMaps::config(1);
+
+            my $pending_route_maps = PVE::Network::SDN::pending_config(
+                { 'route-maps' => { ids => $running_config->list() } },
+                { ids => $current_config->list() },
+                'route-maps',
+            );
+
+            $digest = $current_config->digest();
+            $route_maps = $pending_route_maps->{ids};
+        } elsif ($running) {
+            $route_maps = PVE::Network::SDN::RouteMaps::config(1)->list();
+        } else {
+            my $current_config = PVE::Network::SDN::RouteMaps::config();
+
+            $digest = $current_config->digest();
+            $route_maps = $current_config->list();
+        }
+
+        my $rpcenv = PVE::RPCEnvironment::get();
+        my $authuser = $rpcenv->get_user();
+        my $route_map_privs = ['SDN.Audit', 'SDN.Allocate'];
+
+        my @res;
+        for my $route_map_id (sort keys $route_maps->%*) {
+            next
+                if !$rpcenv->check_any($authuser, "/sdn/route-maps/$route_map_id",
+                    $route_map_privs, 1);
+
+            $route_maps->{$route_map_id}->{digest} = $digest if $digest;
+            push @res, $route_maps->{$route_map_id};
+        }
+
+        return \@res;
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'create_route_map_entry',
+    path => '',
+    method => 'POST',
+    protected => 1,
+    permissions => {
+        check => ['perm', '/sdn/route-maps', ['SDN.Allocate']],
+    },
+    description => "Create Route Map entry",
+    parameters => {
+        properties => {
+            digest => get_standard_option('pve-config-digest'),
+            'lock-token' => get_standard_option('pve-sdn-lock-token'),
+            PVE::Network::SDN::RouteMaps::route_map_properties(0)->%*,
+        },
+    },
+    returns => {
+        type => "null",
+    },
+    code => sub {
+        my ($param) = @_;
+
+        my $lock_token = extract_param($param, 'lock-token');
+
+        PVE::Network::SDN::lock_sdn_config(
+            sub {
+                my $config = PVE::Network::SDN::RouteMaps::config();
+
+                my $digest = extract_param($param, 'digest');
+                PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+                $config->create($param);
+                PVE::Network::SDN::RouteMaps::write_config($config);
+            },
+            "creating route map entry failed",
+            $lock_token,
+        );
+
+        return;
+    },
+});
+
+
diff --git a/src/PVE/API2/Network/SDN/RouteMaps/RouteMapEntry.pm b/src/PVE/API2/Network/SDN/RouteMaps/RouteMapEntry.pm
index f53cfad3..a7561f44 100644
--- a/src/PVE/API2/Network/SDN/RouteMaps/RouteMapEntry.pm
+++ b/src/PVE/API2/Network/SDN/RouteMaps/RouteMapEntry.pm
@@ -3,9 +3,8 @@ package PVE::API2::Network::SDN::RouteMaps::RouteMapEntry;
 use strict;
 use warnings;
 
-use PVE::Exception qw(raise_param_exc);
 use PVE::JSONSchema qw(get_standard_option);
-use PVE::Network::SDN::RouteMaps;
+use PVE::Exception qw(raise_param_exc);
 use PVE::Tools qw(extract_param);
 
 use PVE::RESTHandler;
@@ -13,7 +12,7 @@ use base qw(PVE::RESTHandler);
 
 __PACKAGE__->register_method({
     name => 'get_route_map_entry',
-    path => '',
+    path => '{order}',
     method => 'GET',
     permissions => {
         check =>
@@ -48,7 +47,7 @@ __PACKAGE__->register_method({
 
 __PACKAGE__->register_method({
     name => 'update_route_map_entry',
-    path => '',
+    path => '{order}',
     method => 'PUT',
     protected => 1,
     permissions => {
@@ -94,7 +93,7 @@ __PACKAGE__->register_method({
 
 __PACKAGE__->register_method({
     name => 'delete_route_map_entry',
-    path => '',
+    path => '{order}',
     method => 'DELETE',
     protected => 1,
     permissions => {
-- 
2.47.3





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

* [PATCH pve-network v6 08/24] api: refactor prefix list api structure
  2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
                   ` (6 preceding siblings ...)
  2026-05-08 16:31 ` [PATCH pve-network v6 07/24] api: refactor route map api structure Stefan Hanreich
@ 2026-05-08 16:31 ` Stefan Hanreich
  2026-05-08 16:31 ` [PATCH pve-manager v6 09/24] ui: sdn: add route map selector Stefan Hanreich
                   ` (15 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2026-05-08 16:31 UTC (permalink / raw)
  To: pve-devel

The existing prefix list api structure has been adapted as follows:

GET /prefix-lists
Added a verbose parameter. If it is set, then all properties of the
prefix list are returned, otherwise only the id and state (if
pending=1) are returned from the endpoint.

This commit adds CRUD endpoints for manipulating entries of prefix
lists in the /prefix-lists/{id}/entries subfolder:

GET /prefix-lists/{id}/entries/{seq}
Returns the entry with sequence number {seq}.

POST /prefix-lists/{id}/entries
Creates a new entry in the prefix list. If the sequnce number is given
in the body, then that sequence number is used - otherwise a sequence
number will be auto-generated by taking the highest existing sequence
number and adding 5.

PUT /prefix-lists/{id}/entries/{seq}
Updates the entry with sequence number {seq}. If the body contains the
seq field and it is different from the sequence number given in the
URL, then the sequence number will be changed as wel;.

DELETE /prefix-lists/{id}/entries/{seq}
Deletes the entry with sequence number {seq}.

In order to reuse the schema from the prefix list endpoints, a new
method has been added that allows sharing the prefix list entry
properties among the old and new API endpoints.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 src/PVE/API2/Network/SDN/Makefile             |   1 +
 src/PVE/API2/Network/SDN/PrefixLists.pm       | 145 +++---------
 src/PVE/API2/Network/SDN/PrefixLists/Makefile |   9 +
 .../Network/SDN/PrefixLists/PrefixList.pm     | 139 ++++++++++++
 .../SDN/PrefixLists/PrefixListEntry.pm        | 208 ++++++++++++++++++
 src/PVE/Network/SDN/PrefixLists.pm            |  80 ++++---
 6 files changed, 432 insertions(+), 150 deletions(-)
 create mode 100644 src/PVE/API2/Network/SDN/PrefixLists/Makefile
 create mode 100644 src/PVE/API2/Network/SDN/PrefixLists/PrefixList.pm
 create mode 100644 src/PVE/API2/Network/SDN/PrefixLists/PrefixListEntry.pm

diff --git a/src/PVE/API2/Network/SDN/Makefile b/src/PVE/API2/Network/SDN/Makefile
index 770eef24..6b91f8cc 100644
--- a/src/PVE/API2/Network/SDN/Makefile
+++ b/src/PVE/API2/Network/SDN/Makefile
@@ -17,4 +17,5 @@ install:
 	make -C Fabrics install
 	make -C Nodes install
 	make -C RouteMaps install
+	make -C PrefixLists install
 
diff --git a/src/PVE/API2/Network/SDN/PrefixLists.pm b/src/PVE/API2/Network/SDN/PrefixLists.pm
index f2e14d1d..7bd85746 100644
--- a/src/PVE/API2/Network/SDN/PrefixLists.pm
+++ b/src/PVE/API2/Network/SDN/PrefixLists.pm
@@ -3,6 +3,7 @@ package PVE::API2::Network::SDN::PrefixLists;
 use strict;
 use warnings;
 
+use PVE::API2::Network::SDN::PrefixLists::PrefixList;
 use PVE::Exception qw(raise_param_exc);
 use PVE::JSONSchema qw(get_standard_option);
 use PVE::Network::SDN::PrefixLists;
@@ -11,6 +12,11 @@ use PVE::Tools qw(extract_param);
 use PVE::RESTHandler;
 use base qw(PVE::RESTHandler);
 
+__PACKAGE__->register_method({
+    subclass => "PVE::API2::Network::SDN::PrefixLists::PrefixList",
+    path => '{id}',
+});
+
 __PACKAGE__->register_method({
     name => 'list_prefix_lists',
     path => '',
@@ -33,6 +39,11 @@ __PACKAGE__->register_method({
                 optional => 1,
                 description => "Display pending config.",
             },
+            verbose => {
+                type => 'boolean',
+                optional => 1,
+                description => "If 0, only returns id - otherwise returns all properties.",
+            },
         },
     },
     returns => {
@@ -48,6 +59,7 @@ __PACKAGE__->register_method({
 
         my $pending = extract_param($param, 'pending');
         my $running = extract_param($param, 'running');
+        my $verbose = extract_param($param, 'verbose');
 
         my $digest;
         my $prefix_lists;
@@ -86,41 +98,23 @@ __PACKAGE__->register_method({
                     $prefix_list_privs,
                     1,
                 );
-            $prefix_lists->{$prefix_list_id}->{digest} = $digest if $digest;
-            push @res, $prefix_lists->{$prefix_list_id};
-        }
-
-        return \@res;
-    },
-});
 
-__PACKAGE__->register_method({
-    name => 'get_prefix_list_entry',
-    path => '{id}',
-    method => 'GET',
-    permissions => {
-        check => ['perm', '/sdn/prefix-lists/{id}', ['SDN.Audit']],
-    },
-    description => "Get Prefix List",
-    parameters => {
-        properties => {
-            id => get_standard_option('pve-sdn-prefix-list-id'),
-        },
-    },
-    returns => {
-        type => "object",
-        properties => {},
-    },
-    code => sub {
-        my ($param) = @_;
+            if ($verbose) {
+                $prefix_lists->{$prefix_list_id}->{digest} = $digest if $digest;
+                push @res, $prefix_lists->{$prefix_list_id};
+            } else {
+                my $data = {
+                    id => $prefix_list_id,
+                };
 
-        my $prefix_list_id = extract_param($param, 'id');
-        my $prefix_list_entry = PVE::Network::SDN::PrefixLists::config()->get($prefix_list_id);
+                $data->{state} = $prefix_lists->{$prefix_list_id}->{state}
+                    if $pending && $prefix_lists->{$prefix_list_id}->{state};
 
-        raise_param_exc({ 'id' => "$prefix_list_id doesn't exist" })
-            if !$prefix_list_entry;
+                push @res, $data;
+            }
+        }
 
-        return $prefix_list_entry;
+        return \@res;
     },
 });
 
@@ -166,93 +160,4 @@ __PACKAGE__->register_method({
     },
 });
 
-__PACKAGE__->register_method({
-    name => 'update_prefix_list_entry',
-    path => '{id}',
-    method => 'PUT',
-    protected => 1,
-    permissions => {
-        check => ['perm', '/sdn/prefix-lists/{id}', ['SDN.Allocate']],
-    },
-    description => "Update Prefix List",
-    parameters => {
-        properties => {
-            digest => get_standard_option('pve-config-digest'),
-            'lock-token' => get_standard_option('pve-sdn-lock-token'),
-            PVE::Network::SDN::PrefixLists::prefix_list_properties(1)->%*,
-        },
-    },
-    returns => {
-        type => "null",
-    },
-    code => sub {
-        my ($param) = @_;
-
-        my $lock_token = extract_param($param, 'lock-token');
-
-        PVE::Network::SDN::lock_sdn_config(
-            sub {
-                my $config = PVE::Network::SDN::PrefixLists::config();
-
-                my $digest = extract_param($param, 'digest');
-                PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
-
-                my $prefix_list_id = extract_param($param, 'id');
-                my $delete = extract_param($param, 'delete');
-
-                $config->update($prefix_list_id, $param, $delete);
-                PVE::Network::SDN::PrefixLists::write_config($config);
-            },
-            "updating prefix list failed",
-            $lock_token,
-        );
-
-        return;
-    },
-});
-
-__PACKAGE__->register_method({
-    name => 'delete_prefix_list_entry',
-    path => '{id}',
-    method => 'DELETE',
-    protected => 1,
-    permissions => {
-        check => ['perm', '/sdn/prefix-lists/{id}', ['SDN.Allocate']],
-    },
-    description => "Delete Prefix List",
-    parameters => {
-        properties => {
-            id => get_standard_option('pve-sdn-prefix-list-id'),
-            'lock-token' => get_standard_option('pve-sdn-lock-token'),
-        },
-    },
-    returns => {
-        type => "null",
-    },
-    code => sub {
-        my ($param) = @_;
-
-        my $lock_token = extract_param($param, 'lock-token');
-
-        PVE::Network::SDN::lock_sdn_config(
-            sub {
-                my $config = PVE::Network::SDN::PrefixLists::config();
-
-                my $digest = extract_param($param, 'digest');
-                PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
-
-                my $prefix_list_id = extract_param($param, 'id');
-                PVE::Network::SDN::PrefixLists::check_references($prefix_list_id);
-
-                $config->delete($prefix_list_id);
-                PVE::Network::SDN::PrefixLists::write_config($config);
-            },
-            "deleting prefix list failed",
-            $lock_token,
-        );
-
-        return;
-    },
-});
-
 1;
diff --git a/src/PVE/API2/Network/SDN/PrefixLists/Makefile b/src/PVE/API2/Network/SDN/PrefixLists/Makefile
new file mode 100644
index 00000000..815a4b09
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/PrefixLists/Makefile
@@ -0,0 +1,9 @@
+SOURCES=PrefixList.pm\
+		PrefixListEntry.pm
+
+
+PERL5DIR=${DESTDIR}/usr/share/perl5
+
+.PHONY: install
+install:
+	for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/API2/Network/SDN/PrefixLists/$$i; done
diff --git a/src/PVE/API2/Network/SDN/PrefixLists/PrefixList.pm b/src/PVE/API2/Network/SDN/PrefixLists/PrefixList.pm
new file mode 100644
index 00000000..d0332441
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/PrefixLists/PrefixList.pm
@@ -0,0 +1,139 @@
+package PVE::API2::Network::SDN::PrefixLists::PrefixList;
+
+use strict;
+use warnings;
+
+use PVE::API2::Network::SDN::PrefixLists::PrefixListEntry;
+use PVE::Exception qw(raise_param_exc);
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::Network::SDN::PrefixLists;
+use PVE::Tools qw(extract_param);
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method({
+    subclass => "PVE::API2::Network::SDN::PrefixLists::PrefixListEntry",
+    path => 'entries',
+});
+
+__PACKAGE__->register_method({
+    name => 'get_prefix_list',
+    path => '',
+    method => 'GET',
+    permissions => {
+        check => ['perm', '/sdn/prefix-lists/{id}', ['SDN.Audit']],
+    },
+    description => "Get Prefix List",
+    parameters => {
+        properties => {
+            id => get_standard_option('pve-sdn-prefix-list-id'),
+        },
+    },
+    returns => {
+        type => "object",
+        properties => {},
+    },
+    code => sub {
+        my ($param) = @_;
+
+        my $prefix_list_id = extract_param($param, 'id');
+        my $prefix_list_entry = PVE::Network::SDN::PrefixLists::config()->get($prefix_list_id);
+
+        raise_param_exc({ 'id' => "$prefix_list_id doesn't exist" })
+            if !$prefix_list_entry;
+
+        return $prefix_list_entry;
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'update_prefix_list',
+    path => '',
+    method => 'PUT',
+    protected => 1,
+    permissions => {
+        check => ['perm', '/sdn/prefix-lists/{id}', ['SDN.Allocate']],
+    },
+    description => "Update Prefix List",
+    parameters => {
+        properties => {
+            digest => get_standard_option('pve-config-digest'),
+            'lock-token' => get_standard_option('pve-sdn-lock-token'),
+            PVE::Network::SDN::PrefixLists::prefix_list_properties(1)->%*,
+        },
+    },
+    returns => {
+        type => "null",
+    },
+    code => sub {
+        my ($param) = @_;
+
+        my $lock_token = extract_param($param, 'lock-token');
+
+        PVE::Network::SDN::lock_sdn_config(
+            sub {
+                my $config = PVE::Network::SDN::PrefixLists::config();
+
+                my $digest = extract_param($param, 'digest');
+                PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+                my $prefix_list_id = extract_param($param, 'id');
+                my $delete = extract_param($param, 'delete');
+
+                $config->update($prefix_list_id, $param, $delete);
+                PVE::Network::SDN::PrefixLists::write_config($config);
+            },
+            "updating prefix list failed",
+            $lock_token,
+        );
+
+        return;
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'delete_prefix_list',
+    path => '',
+    method => 'DELETE',
+    protected => 1,
+    permissions => {
+        check => ['perm', '/sdn/prefix-lists/{id}', ['SDN.Allocate']],
+    },
+    description => "Delete Prefix List",
+    parameters => {
+        properties => {
+            id => get_standard_option('pve-sdn-prefix-list-id'),
+            'lock-token' => get_standard_option('pve-sdn-lock-token'),
+        },
+    },
+    returns => {
+        type => "null",
+    },
+    code => sub {
+        my ($param) = @_;
+
+        my $lock_token = extract_param($param, 'lock-token');
+
+        PVE::Network::SDN::lock_sdn_config(
+            sub {
+                my $config = PVE::Network::SDN::PrefixLists::config();
+
+                my $digest = extract_param($param, 'digest');
+                PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+                my $prefix_list_id = extract_param($param, 'id');
+                PVE::Network::SDN::PrefixLists::check_references($prefix_list_id);
+
+                $config->delete($prefix_list_id);
+                PVE::Network::SDN::PrefixLists::write_config($config);
+            },
+            "deleting prefix list failed",
+            $lock_token,
+        );
+
+        return;
+    },
+});
+
+1;
diff --git a/src/PVE/API2/Network/SDN/PrefixLists/PrefixListEntry.pm b/src/PVE/API2/Network/SDN/PrefixLists/PrefixListEntry.pm
new file mode 100644
index 00000000..612fdb92
--- /dev/null
+++ b/src/PVE/API2/Network/SDN/PrefixLists/PrefixListEntry.pm
@@ -0,0 +1,208 @@
+package PVE::API2::Network::SDN::PrefixLists::PrefixListEntry;
+
+use strict;
+use warnings;
+
+use PVE::Exception qw(raise_param_exc);
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::Network::SDN::PrefixLists;
+use PVE::Tools qw(extract_param);
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method({
+    name => 'get_prefix_list_entries',
+    path => '',
+    method => 'GET',
+    permissions => {
+        check => ['perm', '/sdn/prefix-lists/{id}', ['SDN.Audit']],
+    },
+    description => "Get Prefix List",
+    parameters => {
+        properties => {
+            id => get_standard_option('pve-sdn-prefix-list-id'),
+        },
+    },
+    returns => {
+        type => 'array',
+        items => {
+            type => "object",
+            properties => {},
+        },
+        links => [{ rel => 'child', href => "{seq}" }],
+    },
+    code => sub {
+        my ($param) = @_;
+
+        my $prefix_list_id = extract_param($param, 'id');
+        return PVE::Network::SDN::PrefixLists::config()->list_entries($prefix_list_id);
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'get_prefix_list_entry',
+    path => '{url_seq}',
+    method => 'GET',
+    permissions => {
+        check => ['perm', '/sdn/prefix-lists/{id}', ['SDN.Audit']],
+    },
+    description => "Get Prefix List",
+    parameters => {
+        properties => {
+            id => get_standard_option('pve-sdn-prefix-list-id'),
+        },
+    },
+    returns => {
+        type => "object",
+        properties => {},
+    },
+    code => sub {
+        my ($param) = @_;
+
+        my $prefix_list_id = extract_param($param, 'id');
+        my $seq_nr = extract_param($param, 'url_seq');
+        my $prefix_list_entry = PVE::Network::SDN::PrefixLists::config()->get_entry($prefix_list_id, $seq_nr);
+
+        raise_param_exc({ 'id' => "entry $seq_nr in prefix list $prefix_list_id doesn't exist" })
+            if !$prefix_list_entry;
+
+        return $prefix_list_entry;
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'update_prefix_list_entry',
+    path => '{url_seq}',
+    method => 'PUT',
+    protected => 1,
+    permissions => {
+        check => ['perm', '/sdn/prefix-lists/{id}', ['SDN.Allocate']],
+    },
+    description => "Update Prefix List",
+    parameters => {
+        properties => {
+            digest => get_standard_option('pve-config-digest'),
+            'lock-token' => get_standard_option('pve-sdn-lock-token'),
+            PVE::Network::SDN::PrefixLists::prefix_list_entry_properties(1, 1)->%*,
+        },
+    },
+    returns => {
+        type => "null",
+    },
+    code => sub {
+        my ($param) = @_;
+
+        my $lock_token = extract_param($param, 'lock-token');
+
+        PVE::Network::SDN::lock_sdn_config(
+            sub {
+                my $config = PVE::Network::SDN::PrefixLists::config();
+
+                my $digest = extract_param($param, 'digest');
+                PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+                my $prefix_list_id = extract_param($param, 'id');
+                my $old_seq = extract_param($param, 'url_seq');
+                my $delete = extract_param($param, 'delete');
+
+                $config->update_entry($prefix_list_id, $old_seq, $param, $delete);
+                PVE::Network::SDN::PrefixLists::write_config($config);
+            },
+            "updating prefix list entry failed",
+            $lock_token,
+        );
+
+        return;
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'delete_prefix_list_entry',
+    path => '{url_seq}',
+    method => 'DELETE',
+    protected => 1,
+    permissions => {
+        check => ['perm', '/sdn/prefix-lists/{id}', ['SDN.Allocate']],
+    },
+    description => "Delete Prefix List",
+    parameters => {
+        properties => {
+            id => get_standard_option('pve-sdn-prefix-list-id'),
+            'lock-token' => get_standard_option('pve-sdn-lock-token'),
+        },
+    },
+    returns => {
+        type => "null",
+    },
+    code => sub {
+        my ($param) = @_;
+
+        my $lock_token = extract_param($param, 'lock-token');
+
+        PVE::Network::SDN::lock_sdn_config(
+            sub {
+                my $config = PVE::Network::SDN::PrefixLists::config();
+
+                my $digest = extract_param($param, 'digest');
+                PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+                my $prefix_list_id = extract_param($param, 'id');
+                my $seq_nr = extract_param($param, 'url_seq');
+
+                $config->delete_entry($prefix_list_id, $seq_nr);
+                PVE::Network::SDN::PrefixLists::write_config($config);
+            },
+            "deleting prefix list entry failed",
+            $lock_token,
+        );
+
+        return;
+    },
+});
+
+__PACKAGE__->register_method({
+    name => 'create_prefix_list_entry',
+    path => '',
+    method => 'POST',
+    protected => 1,
+    permissions => {
+        check => ['perm', '/sdn/prefix-lists/{id}', ['SDN.Allocate']],
+    },
+    description => "Delete Prefix List",
+    parameters => {
+        properties => {
+            id => get_standard_option('pve-sdn-prefix-list-id'),
+            'lock-token' => get_standard_option('pve-sdn-lock-token'),
+            PVE::Network::SDN::PrefixLists::prefix_list_entry_properties(0, 1)->%*,
+        },
+    },
+    returns => {
+        type => "null",
+    },
+    code => sub {
+        my ($param) = @_;
+
+        my $lock_token = extract_param($param, 'lock-token');
+
+        PVE::Network::SDN::lock_sdn_config(
+            sub {
+                my $config = PVE::Network::SDN::PrefixLists::config();
+
+                my $digest = extract_param($param, 'digest');
+                PVE::Tools::assert_if_modified($config->digest(), $digest) if $digest;
+
+                my $prefix_list_id = extract_param($param, 'id');
+
+                $config->create_entry($prefix_list_id, $param);
+                PVE::Network::SDN::PrefixLists::write_config($config);
+            },
+            "creating prefix list entry failed",
+            $lock_token,
+        );
+
+        return;
+    },
+});
+
+1;
diff --git a/src/PVE/Network/SDN/PrefixLists.pm b/src/PVE/Network/SDN/PrefixLists.pm
index efe1463e..3b27d61b 100644
--- a/src/PVE/Network/SDN/PrefixLists.pm
+++ b/src/PVE/Network/SDN/PrefixLists.pm
@@ -105,44 +105,66 @@ sub check_references {
     }
 }
 
+sub prefix_list_entry_properties {
+    my ($update, $standalone) = @_;
+
+    my $properties = {
+        action => {
+            type => 'string',
+            enum => ['permit', 'deny'],
+            optional => $update,
+        },
+        prefix => {
+            type => 'string',
+            format => 'CIDR',
+            optional => $update,
+        },
+        le => {
+            type => 'integer',
+            minimum => 0,
+            maximum => 128,
+            optional => 1,
+        },
+        ge => {
+            type => 'integer',
+            minimum => 0,
+            maximum => 128,
+            optional => 1,
+        },
+        seq => {
+            type => 'integer',
+            minimum => 0,
+            maximum => 2**32 - 1,
+            optional => 1,
+        },
+    };
+
+    if ($update && $standalone) {
+        $properties->{delete} = {
+            type => 'array',
+            optional => 1,
+            items => {
+                type => 'string',
+                enum => ['le', 'ge', 'seq'],
+            },
+        };
+    }
+
+    return $properties;
+}
+
 sub prefix_list_properties {
     my ($update) = @_;
 
     my $properties = {
+        id => get_standard_option('pve-sdn-prefix-list-id'),
         digest => get_standard_option('pve-config-digest'),
         entries => {
             type => 'array',
             optional => 1,
             items => {
                 type => 'string',
-                format => {
-                    action => {
-                        type => 'string',
-                        enum => ['permit', 'deny'],
-                    },
-                    prefix => {
-                        type => 'string',
-                        format => 'CIDR',
-                    },
-                    le => {
-                        type => 'integer',
-                        minimum => 0,
-                        maximum => 128,
-                        optional => 1,
-                    },
-                    ge => {
-                        type => 'integer',
-                        minimum => 0,
-                        maximum => 128,
-                        optional => 1,
-                    },
-                    seq => {
-                        type => 'integer',
-                        minimum => 0,
-                        maximum => 2**32 - 1,
-                        optional => 1,
-                    },
-                },
+                format => prefix_list_entry_properties($update, 0),
             },
         },
     };
@@ -156,8 +178,6 @@ sub prefix_list_properties {
                 enum => ['entries'],
             },
         };
-    } else {
-        $properties->{id} = get_standard_option('pve-sdn-prefix-list-id');
     }
 
     return $properties;
-- 
2.47.3





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

* [PATCH pve-manager v6 09/24] ui: sdn: add route map selector
  2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
                   ` (7 preceding siblings ...)
  2026-05-08 16:31 ` [PATCH pve-network v6 08/24] api: refactor prefix list " Stefan Hanreich
@ 2026-05-08 16:31 ` Stefan Hanreich
  2026-05-08 16:31 ` [PATCH pve-manager v6 10/24] ui: sdn: add prefix list selector Stefan Hanreich
                   ` (14 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2026-05-08 16:31 UTC (permalink / raw)
  To: pve-devel; +Cc: Thomas Lamprecht

A UI component that provides a dropdown for selecting route maps. This
will be used in various places that allow defining a route map, e.g.
in the EVPN / BGP controller or when configuring route
redistributions for fabrics.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Link: https://lore.proxmox.com/20260505153720.412180-40-s.hanreich@proxmox.com
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
---
 www/manager6/Makefile                |  1 +
 www/manager6/sdn/RouteMapSelector.js | 56 ++++++++++++++++++++++++++++
 2 files changed, 57 insertions(+)
 create mode 100644 www/manager6/sdn/RouteMapSelector.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index f63437d6f..ed380b632 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -309,6 +309,7 @@ JSSRC= 							\
 	sdn/ZoneView.js					\
 	sdn/IpamEdit.js					\
 	sdn/OptionsPanel.js				\
+	sdn/RouteMapSelector.js				\
 	sdn/controllers/Base.js				\
 	sdn/controllers/EvpnEdit.js			\
 	sdn/controllers/BgpEdit.js			\
diff --git a/www/manager6/sdn/RouteMapSelector.js b/www/manager6/sdn/RouteMapSelector.js
new file mode 100644
index 000000000..3a2770798
--- /dev/null
+++ b/www/manager6/sdn/RouteMapSelector.js
@@ -0,0 +1,56 @@
+Ext.define('PVE.sdn.RouteMap', {
+    extend: 'Ext.data.Model',
+    fields: ['id'],
+});
+
+Ext.define('PVE.sdn.RouteMapSelector', {
+    extend: 'Proxmox.form.ComboGrid',
+    alias: 'widget.pveSDNRouteMapSelector',
+
+    displayField: 'id',
+
+    deleteEmpty: true,
+    editable: false,
+    allowBlank: true,
+    autoSelect: false,
+
+    config: {
+        value: null,
+    },
+
+    store: {
+        autoLoad: true,
+        model: 'PVE.sdn.RouteMap',
+        proxy: {
+            type: 'proxmox',
+            url: '/api2/json/cluster/sdn/route-maps',
+            reader: {
+                transform: {
+                    fn: function (response) {
+                        return Object.values(
+                            response.data.reduce((accumulator, routeMapEntry) => {
+                                let id = routeMapEntry['route-map-id'];
+
+                                accumulator[id] ??= {
+                                    id,
+                                };
+
+                                return accumulator;
+                            }, {}),
+                        );
+                    },
+                },
+            },
+        },
+    },
+    listConfig: {
+        columns: [
+            {
+                header: gettext('Name'),
+                dataIndex: 'id',
+                hideable: false,
+                flex: 1,
+            },
+        ],
+    },
+});
-- 
2.47.3





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

* [PATCH pve-manager v6 10/24] ui: sdn: add prefix list selector
  2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
                   ` (8 preceding siblings ...)
  2026-05-08 16:31 ` [PATCH pve-manager v6 09/24] ui: sdn: add route map selector Stefan Hanreich
@ 2026-05-08 16:31 ` Stefan Hanreich
  2026-05-08 16:31 ` [PATCH pve-manager v6 11/24] ui: sdn: add panel for managing prefix lists Stefan Hanreich
                   ` (13 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2026-05-08 16:31 UTC (permalink / raw)
  To: pve-devel; +Cc: Thomas Lamprecht

A UI component that provides a dropdown for selecting prefix lists.
This will be used in various places that allow defining a prefix
lists, e.g. when applying route filtering to fabric settings.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Link: https://lore.proxmox.com/20260505153720.412180-41-s.hanreich@proxmox.com
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
---
 www/manager6/Makefile                  |  1 +
 www/manager6/sdn/PrefixListSelector.js | 30 ++++++++++++++++++++++++++
 2 files changed, 31 insertions(+)
 create mode 100644 www/manager6/sdn/PrefixListSelector.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index ed380b632..615e68662 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -310,6 +310,7 @@ JSSRC= 							\
 	sdn/IpamEdit.js					\
 	sdn/OptionsPanel.js				\
 	sdn/RouteMapSelector.js				\
+	sdn/PrefixListSelector.js				\
 	sdn/controllers/Base.js				\
 	sdn/controllers/EvpnEdit.js			\
 	sdn/controllers/BgpEdit.js			\
diff --git a/www/manager6/sdn/PrefixListSelector.js b/www/manager6/sdn/PrefixListSelector.js
new file mode 100644
index 000000000..26f8b3d35
--- /dev/null
+++ b/www/manager6/sdn/PrefixListSelector.js
@@ -0,0 +1,30 @@
+Ext.define('PVE.sdn.PrefixListSelector', {
+    extend: 'Proxmox.form.ComboGrid',
+    alias: 'widget.pveSDNPrefixListSelector',
+
+    displayField: 'id',
+
+    deleteEmpty: true,
+    editable: false,
+    allowBlank: true,
+    autoSelect: false,
+
+    store: {
+        autoLoad: true,
+        model: 'PVE.sdn.PrefixList',
+        proxy: {
+            type: 'proxmox',
+            url: '/api2/json/cluster/sdn/prefix-lists',
+        },
+    },
+    listConfig: {
+        columns: [
+            {
+                header: gettext('Name'),
+                dataIndex: 'id',
+                hideable: false,
+                flex: 1,
+            },
+        ],
+    },
+});
-- 
2.47.3





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

* [PATCH pve-manager v6 11/24] ui: sdn: add panel for managing prefix lists
  2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
                   ` (9 preceding siblings ...)
  2026-05-08 16:31 ` [PATCH pve-manager v6 10/24] ui: sdn: add prefix list selector Stefan Hanreich
@ 2026-05-08 16:31 ` Stefan Hanreich
  2026-05-08 16:31 ` [PATCH pve-manager v6 12/24] ui: sdn: add panel for managing route map entries Stefan Hanreich
                   ` (12 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2026-05-08 16:31 UTC (permalink / raw)
  To: pve-devel; +Cc: Thomas Lamprecht

This panel allows users to perform CRUD operations for prefix lists
and their entries, as well as re-ordering prefix lists entries. It
allows editing each entry in a prefix list separately via an edit
window even though they are a single property in the section config.
This is implemented by sending the full entries property to the update
endpoint everytime a single entry has been edited.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Link: https://lore.proxmox.com/20260505153720.412180-42-s.hanreich@proxmox.com
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
---
 www/manager6/Makefile               |   1 +
 www/manager6/dc/Config.js           |   8 +
 www/manager6/sdn/PrefixListPanel.js | 459 ++++++++++++++++++++++++++++
 3 files changed, 468 insertions(+)
 create mode 100644 www/manager6/sdn/PrefixListPanel.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 615e68662..b123a331d 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -310,6 +310,7 @@ JSSRC= 							\
 	sdn/IpamEdit.js					\
 	sdn/OptionsPanel.js				\
 	sdn/RouteMapSelector.js				\
+	sdn/PrefixListPanel.js				\
 	sdn/PrefixListSelector.js				\
 	sdn/controllers/Base.js				\
 	sdn/controllers/EvpnEdit.js			\
diff --git a/www/manager6/dc/Config.js b/www/manager6/dc/Config.js
index b5e27a212..8784e357c 100644
--- a/www/manager6/dc/Config.js
+++ b/www/manager6/dc/Config.js
@@ -312,6 +312,14 @@ Ext.define('PVE.dc.Config', {
                         iconCls: 'fa fa-road',
                         itemId: 'sdnfabrics',
                     },
+                    {
+                        xtype: 'pveSDNPrefixLists',
+                        groups: ['sdn'],
+                        title: gettext('Prefix Lists'),
+                        hidden: true,
+                        iconCls: 'fa fa-list-ol',
+                        itemId: 'sdnprefixlists',
+                    },
                 );
             }
 
diff --git a/www/manager6/sdn/PrefixListPanel.js b/www/manager6/sdn/PrefixListPanel.js
new file mode 100644
index 000000000..7152058e7
--- /dev/null
+++ b/www/manager6/sdn/PrefixListPanel.js
@@ -0,0 +1,459 @@
+Ext.define('PVE.sdn.PrefixList', {
+    extend: 'Ext.data.Model',
+    fields: ['id', 'entries', 'pending'],
+
+    getId: function () {
+        let me = this;
+        return me.data.pending?.[me.idProperty] ?? me.data[me.idProperty];
+    },
+});
+
+Ext.define('PVE.sdn.PrefixListEntry', {
+    extend: 'Ext.data.Model',
+    fields: ['id', 'action', 'prefix', 'le', 'ge', 'pending'],
+});
+
+Ext.define('PVE.sdn.EditPrefixListWindow', {
+    extend: 'Proxmox.window.Edit',
+
+    url: '/cluster/sdn/prefix-lists',
+
+    config: {
+        entry: null,
+    },
+
+    isCreate: false,
+
+    items: [
+        {
+            xtype: 'proxmoxtextfield',
+            fieldLabel: gettext('Name'),
+            name: 'id',
+        },
+    ],
+
+    initComponent: function () {
+        let me = this;
+        me.method = me.isCreate ? 'POST' : 'PUT';
+        me.callParent();
+
+        me.setValues(me.getEntry());
+    },
+});
+
+Ext.define('PVE.sdn.EditPrefixListEntryWindow', {
+    extend: 'Proxmox.window.Edit',
+
+    url: '/cluster/sdn/prefix-lists',
+
+    config: {
+        entry: null,
+    },
+
+    isCreate: false,
+
+    items: [
+        {
+            xtype: 'proxmoxKVComboBox',
+            fieldLabel: gettext('Action'),
+            name: 'action',
+            comboItems: [
+                ['permit', gettext('Permit')],
+                ['deny', gettext('Deny')],
+            ],
+            allowBlank: false,
+        },
+        {
+            xtype: 'proxmoxtextfield',
+            fieldLabel: gettext('Prefix'),
+            name: 'prefix',
+            vtype: 'IP64CIDRAddress',
+        },
+        {
+            xtype: 'proxmoxtextfield',
+            fieldLabel: gettext('Prefix <='),
+            name: 'le',
+        },
+        {
+            xtype: 'proxmoxtextfield',
+            fieldLabel: gettext('Prefix >='),
+            name: 'ge',
+        },
+    ],
+
+    initComponent: function () {
+        let me = this;
+        me.method = me.isCreate ? 'POST' : 'PUT';
+        me.callParent();
+
+        me.setValues(me.getEntry());
+    },
+});
+
+Ext.define('PVE.sdn.PrefixListView', {
+    extend: 'Ext.grid.GridPanel',
+    alias: ['widget.pveSDNPrefixListView'],
+
+    emptyText: gettext('No prefix list configured'),
+
+    tbar: [
+        {
+            text: gettext('Add'),
+            xtype: 'button',
+            handler: 'addPrefixList',
+        },
+        {
+            text: gettext('Remove'),
+            xtype: 'button',
+            handler: 'removePrefixList',
+            bind: {
+                disabled: '{!prefixListGrid.selection}',
+            },
+        },
+        {
+            text: gettext('Reload'),
+            xtype: 'button',
+            handler: 'reload',
+        },
+    ],
+
+    columns: [
+        {
+            text: gettext('Name'),
+            dataIndex: 'id',
+            flex: 1,
+            renderer: function (value, metaData, rec) {
+                return PVE.Utils.render_sdn_pending(rec, value, 'id', 1);
+            },
+        },
+        {
+            text: gettext('State'),
+            width: 100,
+            dataIndex: 'state',
+            renderer: function (value, metaData, rec) {
+                return PVE.Utils.render_sdn_pending_state(rec, value);
+            },
+        },
+    ],
+});
+
+Ext.define('PVE.sdn.PrefixListEntriesView', {
+    extend: 'Ext.grid.GridPanel',
+    alias: ['widget.pveSDNPrefixListEntriesView'],
+
+    emptyText: gettext('Prefix List has no entries configured.'),
+
+    config: {
+        prefixList: null,
+    },
+
+    viewConfig: {
+        plugins: [
+            {
+                ptype: 'gridviewdragdrop',
+            },
+        ],
+    },
+
+    listeners: {
+        drop: 'saveEntries',
+        itemdblclick: 'editPrefixListEntry',
+    },
+
+    columns: [
+        {
+            width: 40,
+            resizable: false,
+            sortable: false,
+            hideable: false,
+            menuDisabled: true,
+            renderer: function (value, metaData, record, rowIdx, colIdx) {
+                metaData.tdCls = Ext.baseCSSPrefix + 'grid-cell-special';
+                return "<i class='pve-grid-fa fa fa-fw fa-reorder cursor-move'></i>";
+            },
+        },
+        {
+            text: gettext('Action'),
+            dataIndex: 'action',
+            flex: 1,
+        },
+        {
+            text: gettext('Prefix'),
+            dataIndex: 'prefix',
+            flex: 1,
+        },
+        {
+            text: gettext('Prefix <='),
+            dataIndex: 'le',
+            flex: 1,
+        },
+        {
+            text: gettext('Prefix >='),
+            dataIndex: 'ge',
+            flex: 1,
+        },
+    ],
+
+    tbar: [
+        {
+            text: gettext('Add'),
+            xtype: 'button',
+            handler: 'addPrefixListEntry',
+            bind: {
+                disabled: '{!prefixListGrid.selection}',
+            },
+        },
+        {
+            text: gettext('Edit'),
+            xtype: 'button',
+            handler: 'editPrefixListEntry',
+            bind: {
+                disabled: '{!prefixListEntriesGrid.selection}',
+            },
+        },
+        {
+            text: gettext('Remove'),
+            xtype: 'button',
+            handler: 'removePrefixListEntry',
+            bind: {
+                disabled: '{!prefixListEntriesGrid.selection}',
+            },
+        },
+    ],
+});
+
+Ext.define('PVE.sdn.PrefixListPanel', {
+    extend: 'Ext.panel.Panel',
+    alias: ['widget.pveSDNPrefixLists'],
+
+    emptyText: gettext('No prefix list configured'),
+
+    viewModel: {
+        stores: {
+            prefixLists: {
+                autoLoad: true,
+                model: 'PVE.sdn.PrefixList',
+                proxy: {
+                    type: 'proxmox',
+                    url: '/api2/json/cluster/sdn/prefix-lists?pending=1',
+                },
+            },
+            prefixListEntries: {
+                model: 'PVE.sdn.PrefixListEntry',
+                proxy: {
+                    type: 'proxmox',
+                    reader: {
+                        transform: {
+                            fn: function (response) {
+                                let entries = response.data.entries ?? [];
+                                return entries.map(PVE.Parser.parsePropertyString);
+                            },
+                        },
+                    },
+                },
+            },
+        },
+        formulas: {
+            entryGridEmptyText: function (get) {
+                let selection = get('prefixListGrid.selection');
+
+                return selection
+                    ? gettext('Prefix List has no entries configured.')
+                    : gettext('no Prefix List selected');
+            },
+        },
+    },
+
+    controller: {
+        reload: function () {
+            let me = this;
+
+            let prefixList = me.getViewModel().get('prefixListGrid.selection');
+
+            me.getViewModel()
+                .getStore('prefixLists')
+                .load((records, _operation, success) => {
+                    if (!success || !prefixList) {
+                        return;
+                    }
+
+                    let newPrefixList = records.find((record) => {
+                        return record.getId() === prefixList.getId();
+                    });
+
+                    me.lookupReference('prefixListGrid').setSelection(newPrefixList);
+                });
+        },
+        saveEntries: function () {
+            let me = this;
+
+            let prefixList = me.getViewModel().get('prefixListGrid.selection');
+
+            let entries = me
+                .getViewModel()
+                .getStore('prefixListEntries')
+                .getData()
+                .items.map((item) => {
+                    let data = item.data;
+                    delete data.id;
+
+                    return PVE.Parser.printPropertyString(data);
+                });
+
+            let params = {};
+
+            if (entries.length > 0) {
+                params.entries = entries;
+            } else {
+                params = { delete: ['entries'] };
+            }
+
+            Proxmox.Async.api2({
+                url: `/api2/extjs/cluster/sdn/prefix-lists/${prefixList.getId()}`,
+                params,
+                method: 'PUT',
+            })
+                .catch(Proxmox.Utils.alertResponseFailure)
+                .finally(() => {
+                    me.reload(prefixList);
+                });
+        },
+        selectPrefixList: function (gridPanel, record, index, options) {
+            let me = this;
+
+            let url = `/api2/extjs/cluster/sdn/prefix-lists/${record.getId()}`;
+            let entryStore = me.getViewModel().getStore('prefixListEntries');
+
+            entryStore.getProxy().setUrl(url);
+            entryStore.load();
+        },
+        addPrefixList: function () {
+            let me = this;
+
+            Ext.create('PVE.sdn.EditPrefixListWindow', {
+                autoShow: true,
+                isCreate: true,
+                listeners: {
+                    close: () => me.reload(),
+                },
+            });
+        },
+        removePrefixList: function () {
+            let me = this;
+            let prefixList = me.getViewModel().get('prefixListGrid.selection');
+
+            Ext.Msg.show({
+                title: gettext('Confirm'),
+                icon: Ext.Msg.WARNING,
+                message: Ext.String.format(gettext('Remove prefix list "{0}"?'), prefixList.getId()),
+                buttons: Ext.Msg.YESNO,
+                defaultFocus: 'no',
+                callback: function (btn) {
+                    if (btn !== 'yes') {
+                        return;
+                    }
+
+                    Proxmox.Async.api2({
+                        url: `/api2/extjs/cluster/sdn/prefix-lists/${prefixList.getId()}`,
+                        method: 'DELETE',
+                    })
+                        .catch(Proxmox.Utils.alertResponseFailure)
+                        .finally(() => {
+                            me.reload(prefixList);
+                        });
+                },
+            });
+        },
+        addPrefixListEntry: function () {
+            let panel = this;
+
+            Ext.create('PVE.sdn.EditPrefixListEntryWindow', {
+                autoShow: true,
+                isCreate: true,
+                submit: function () {
+                    let me = this;
+
+                    panel.getViewModel().getStore('prefixListEntries').add(me.getValues());
+                    panel.saveEntries();
+
+                    me.close();
+                },
+            });
+        },
+        editPrefixListEntry: function () {
+            let panel = this;
+
+            let entry = panel.getViewModel().get('prefixListEntriesGrid.selection');
+
+            if (!entry) {
+                console.warn('no prefix list entry selected!');
+                return;
+            }
+
+            Ext.create('PVE.sdn.EditPrefixListEntryWindow', {
+                autoShow: true,
+                isCreate: false,
+                entry: entry.data,
+                submit: function () {
+                    let me = this;
+                    entry.set(me.getValues());
+
+                    panel.saveEntries();
+
+                    me.close();
+                },
+            });
+        },
+        removePrefixListEntry: function () {
+            let me = this;
+
+            let entry = me.getViewModel().get('prefixListEntriesGrid.selection');
+
+            Ext.Msg.show({
+                title: gettext('Confirm'),
+                icon: Ext.Msg.WARNING,
+                message: gettext('Remove prefix list entry?'),
+                buttons: Ext.Msg.YESNO,
+                defaultFocus: 'no',
+                callback: function (btn) {
+                    if (btn !== 'yes') {
+                        return;
+                    }
+
+                    me.getViewModel().getStore('prefixListEntries').remove(entry);
+                    me.saveEntries();
+                },
+            });
+        },
+    },
+
+    layout: 'border',
+
+    items: [
+        {
+            xtype: 'pveSDNPrefixListView',
+            region: 'west',
+            width: '50%',
+            border: false,
+            split: true,
+            reference: 'prefixListGrid',
+            bind: {
+                store: '{prefixLists}',
+            },
+            listeners: {
+                select: 'selectPrefixList',
+            },
+        },
+        {
+            xtype: 'pveSDNPrefixListEntriesView',
+            region: 'center',
+            border: false,
+            bind: {
+                prefixList: '{prefixListGrid.selection}',
+                store: '{prefixListEntries}',
+                emptyText: '{entryGridEmptyText}',
+            },
+            reference: 'prefixListEntriesGrid',
+        },
+    ],
+});
-- 
2.47.3





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

* [PATCH pve-manager v6 12/24] ui: sdn: add panel for managing route map entries
  2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
                   ` (10 preceding siblings ...)
  2026-05-08 16:31 ` [PATCH pve-manager v6 11/24] ui: sdn: add panel for managing prefix lists Stefan Hanreich
@ 2026-05-08 16:31 ` Stefan Hanreich
  2026-05-08 16:31 ` [PATCH pve-manager v6 13/24] ui: sdn: bgp controller: allow configuring route maps Stefan Hanreich
                   ` (11 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2026-05-08 16:31 UTC (permalink / raw)
  To: pve-devel; +Cc: Thomas Lamprecht

This panel allows users to perform CRUD operations on route map
entries and shows an overview of all existing route map entries.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Link: https://lore.proxmox.com/20260505153720.412180-43-s.hanreich@proxmox.com
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
---
 www/manager6/Makefile             |   1 +
 www/manager6/dc/Config.js         |   8 +
 www/manager6/sdn/RouteMapPanel.js | 977 ++++++++++++++++++++++++++++++
 3 files changed, 986 insertions(+)
 create mode 100644 www/manager6/sdn/RouteMapPanel.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index b123a331d..597769bb9 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -309,6 +309,7 @@ JSSRC= 							\
 	sdn/ZoneView.js					\
 	sdn/IpamEdit.js					\
 	sdn/OptionsPanel.js				\
+	sdn/RouteMapPanel.js				\
 	sdn/RouteMapSelector.js				\
 	sdn/PrefixListPanel.js				\
 	sdn/PrefixListSelector.js				\
diff --git a/www/manager6/dc/Config.js b/www/manager6/dc/Config.js
index 8784e357c..fd3a68a79 100644
--- a/www/manager6/dc/Config.js
+++ b/www/manager6/dc/Config.js
@@ -312,6 +312,14 @@ Ext.define('PVE.dc.Config', {
                         iconCls: 'fa fa-road',
                         itemId: 'sdnfabrics',
                     },
+                    {
+                        xtype: 'pveSDNRouteMaps',
+                        groups: ['sdn'],
+                        title: gettext('Route Maps'),
+                        hidden: true,
+                        iconCls: 'fa fa-map',
+                        itemId: 'sdnroutemaps',
+                    },
                     {
                         xtype: 'pveSDNPrefixLists',
                         groups: ['sdn'],
diff --git a/www/manager6/sdn/RouteMapPanel.js b/www/manager6/sdn/RouteMapPanel.js
new file mode 100644
index 000000000..5fa0475c3
--- /dev/null
+++ b/www/manager6/sdn/RouteMapPanel.js
@@ -0,0 +1,977 @@
+Ext.define('PVE.sdn.RouteMapEntry', {
+    extend: 'Ext.data.Model',
+    fields: ['route-map-id', 'order', 'action', 'match', 'set', 'exit-action', 'pending'],
+
+    getRouteMapId: function () {
+        let me = this;
+        return me.data.pending?.['route-map-id'] ?? me.data['route-map-id'];
+    },
+
+    getOrder: function () {
+        let me = this;
+        return me.data.pending?.order ?? me.data.order;
+    },
+});
+
+Ext.define('PVE.sdn.RouteMapExitAction', {
+    extend: 'Ext.data.Model',
+    fields: ['key', 'value'],
+});
+
+Ext.define('PVE.sdn.RouteMapSet', {
+    extend: 'Ext.data.Model',
+    fields: ['key', 'value'],
+});
+
+Ext.define('PVE.sdn.RouteMapSetValueField', {
+    extend: 'Ext.container.Container',
+    mixins: ['Ext.form.field.Field'],
+
+    alias: ['widget.pveSdnRouteMapSetValueField'],
+
+    layout: 'vbox',
+
+    config: {
+        record: null,
+    },
+
+    publishes: {
+        record: true,
+    },
+
+    defaults: {
+        width: '100%',
+    },
+
+    viewModel: {
+        data: {
+            selectedKey: null,
+        },
+    },
+
+    items: [],
+
+    getWidgetForKey: function (key) {
+        const widgets = {
+            'ip-next-hop-peer-address': {
+                xtype: 'displayfield',
+            },
+            'ip-next-hop': {
+                xtype: 'proxmoxtextfield',
+                vtype: 'IPAddress',
+                bind: {
+                    value: '{record.value}',
+                },
+            },
+            'ip-next-hop-unchanged': {
+                xtype: 'displayfield',
+            },
+            'ip6-next-hop-peer-address': {
+                xtype: 'displayfield',
+            },
+            'ip6-next-hop-prefer-global': {
+                xtype: 'displayfield',
+            },
+            'ip6-next-hop': {
+                xtype: 'proxmoxtextfield',
+                vtype: 'IP6Address',
+                bind: {
+                    value: '{record.value}',
+                },
+            },
+            'local-preference': {
+                xtype: 'proxmoxintegerfield',
+                minValue: 1,
+                maxValue: 2 ** 32 - 1,
+                step: 1,
+                bind: {
+                    value: '{record.value}',
+                },
+            },
+            tag: {
+                xtype: 'proxmoxintegerfield',
+                minValue: 1,
+                maxValue: 2 ** 32 - 1,
+                step: 1,
+                bind: {
+                    value: '{record.value}',
+                },
+            },
+            weight: {
+                xtype: 'proxmoxintegerfield',
+                minValue: 1,
+                maxValue: 2 ** 32 - 1,
+                step: 1,
+                bind: {
+                    value: '{record.value}',
+                },
+            },
+            metric: {
+                xtype: 'proxmoxintegerfield',
+                minValue: 1,
+                maxValue: 2 ** 32 - 1,
+                step: 1,
+                bind: {
+                    value: '{record.value}',
+                },
+            },
+            src: {
+                xtype: 'proxmoxtextfield',
+                vtype: 'IP64Address',
+                bind: {
+                    value: '{record.value}',
+                },
+            },
+        };
+
+        return (
+            widgets[key] ?? {
+                xtype: 'displayfield',
+            }
+        );
+    },
+
+    applyRecord: function (record) {
+        let me = this;
+
+        if (record.data.key === me.getViewModel().get('selectedKey')) {
+            return;
+        }
+        me.getViewModel().set('selectedKey', record.data.key);
+
+        me.removeAll();
+
+        let widget = me.getWidgetForKey(record.data.key);
+
+        if (widget.xtype === 'displayfield') {
+            me.getRecord()?.set('value', null);
+        }
+
+        me.add(widget);
+
+        return record;
+    },
+});
+
+const ROUTE_MAP_SET_ACTION_LABELS = {
+    'ip-next-hop': gettext('IPv4 next-hop'),
+    'ip-next-hop-peer-address': gettext('IPv4 next-hop to peer address'),
+    'ip-next-hop-unchanged': gettext('IPv4 next-hop unchanged'),
+    'ip6-next-hop': gettext('IPv6 next-hop'),
+    'ip6-next-hop-peer-address': gettext('IPv6 next-hop to peer address'),
+    'ip6-next-hop-prefer-global': gettext('IPv6 next-hop to global address'),
+    'local-preference': gettext('Local Preference'),
+    tag: gettext('Tag'),
+    weight: gettext('Weight'),
+    metric: gettext('Metric'),
+    src: gettext('Source'),
+};
+
+Ext.define('PVE.sdn.RouteMapSetField', {
+    extend: 'Ext.grid.Panel',
+    mixins: ['Ext.form.field.Field'],
+    alias: 'widget.pveSdnRouteMapSetField',
+
+    emptyText: gettext('No set actions configured.'),
+
+    isCreate: false,
+
+    store: {
+        model: 'PVE.sdn.RouteMapSet',
+    },
+
+    columns: [
+        {
+            header: gettext('Property'),
+            xtype: 'widgetcolumn',
+            flex: 1,
+            widget: {
+                xtype: 'proxmoxKVComboBox',
+                comboItems: Object.entries(ROUTE_MAP_SET_ACTION_LABELS),
+                allowBlank: false,
+                deleteEmpty: false,
+                bind: {
+                    value: '{record.key}',
+                },
+                listeners: {
+                    select: function (_this, newValue) {
+                        let me = this;
+                        me.getWidgetRecord().set('key', newValue.id);
+                    },
+                },
+            },
+        },
+        {
+            header: gettext('Value'),
+            flex: 1,
+            xtype: 'widgetcolumn',
+            widget: {
+                xtype: 'pveSdnRouteMapSetValueField',
+                bind: {
+                    record: {
+                        bindTo: '{record}',
+                        deep: true,
+                    },
+                },
+            },
+        },
+        {
+            width: 20,
+            xtype: 'actioncolumn',
+            items: [
+                {
+                    tooltip: gettext('Delete'),
+                    handler: 'deleteSet',
+                    iconCls: 'fa critical fa-trash-o',
+                },
+            ],
+        },
+    ],
+
+    initComponent: function () {
+        let me = this;
+        me.callParent();
+
+        me.getStore().on('datachanged', function () {
+            me.fireEvent('dirtychange');
+        });
+    },
+
+    getValue: function () {
+        let me = this;
+
+        return me
+            .getStore()
+            .getData()
+            .items.map((item) => {
+                let data = item.data;
+                delete data.id;
+
+                if (!data.value) {
+                    delete data.value;
+                }
+
+                return PVE.Parser.printPropertyString(data);
+            });
+    },
+
+    setValue: function (value) {
+        let me = this;
+        me.getStore().setData(value.map(PVE.Parser.parsePropertyString));
+    },
+
+    getSubmitData: function () {
+        let me = this;
+        let value = me.getValue();
+
+        if (value.length === 0) {
+            return {
+                delete: [me.getName()],
+            };
+        }
+
+        return {
+            [me.getName()]: value,
+        };
+    },
+
+    tbar: [
+        {
+            xtype: 'button',
+            text: gettext('Add'),
+            handler: 'addEntry',
+        },
+    ],
+
+    controller: {
+        addEntry: function () {
+            let me = this;
+            me.getView().getStore().add({
+                key: null,
+                value: null,
+            });
+        },
+        deleteSet: function (_table, _rI, _cI, _item, _e, record) {
+            let me = this;
+            me.getView().getStore().remove(record);
+        },
+    },
+});
+
+Ext.define('PVE.sdn.RouteMapMatch', {
+    extend: 'Ext.data.Model',
+    fields: ['key', 'value'],
+});
+
+Ext.define('PVE.sdn.RouteMapMatchValueField', {
+    extend: 'Ext.container.Container',
+    mixins: ['Ext.form.field.Field'],
+
+    alias: ['widget.pveSdnRouteMapMatchValueField'],
+
+    layout: 'vbox',
+
+    config: {
+        key: null,
+        record: null,
+    },
+
+    publishes: {
+        record: true,
+    },
+
+    defaults: {
+        name: 'value',
+        width: '100%',
+        bind: {
+            value: '{record.value}',
+        },
+    },
+
+    items: [],
+
+    getWidgetForKey: function (key) {
+        const widgets = {
+            'route-type': {
+                xtype: 'proxmoxKVComboBox',
+                comboItems: [
+                    ['ead', gettext('Ethernet Auto-Discovery (Type 1)')],
+                    ['macip', gettext('MAC/IP Advertisement (Type 2)')],
+                    ['multicast', gettext('Inclusive Multicast (Type 3)')],
+                    ['es', gettext('Ethernet Segment (Type 4)')],
+                    ['prefix', gettext('IP Prefix (Type 5)')],
+                ],
+                allowBlank: false,
+                deleteEmpty: false,
+            },
+            vni: {
+                xtype: 'proxmoxintegerfield',
+                flex: 1,
+                minValue: 1,
+                maxValue: 2 ** 24 - 1,
+                step: 1,
+            },
+            'ip-address-prefix-list': {
+                xtype: 'pveSDNPrefixListSelector',
+            },
+            'ip6-address-prefix-list': {
+                xtype: 'pveSDNPrefixListSelector',
+            },
+            'ip-next-hop-prefix-list': {
+                xtype: 'pveSDNPrefixListSelector',
+            },
+            'ip6-next-hop-prefix-list': {
+                xtype: 'pveSDNPrefixListSelector',
+            },
+            'ip-next-hop-address': {
+                xtype: 'proxmoxtextfield',
+                vtype: 'IPAddress',
+            },
+            'ip6-next-hop-address': {
+                xtype: 'proxmoxtextfield',
+                vtype: 'IP6Address',
+            },
+            metric: {
+                xtype: 'proxmoxintegerfield',
+                minValue: 1,
+                maxValue: 2 ** 32 - 1,
+                step: 1,
+            },
+            'local-preference': {
+                xtype: 'proxmoxintegerfield',
+                minValue: 1,
+                maxValue: 2 ** 32 - 1,
+                step: 1,
+            },
+            peer: {
+                xtype: 'proxmoxtextfield',
+            },
+        };
+
+        return (
+            widgets[key] ?? {
+                xtype: 'displayfield',
+            }
+        );
+    },
+
+    updateKey: function (key) {
+        let me = this;
+
+        me.removeAll();
+        me.add(me.getWidgetForKey(key));
+
+        return key;
+    },
+});
+
+const ROUTE_MAP_MATCH_ACTION_LABELS = {
+    'route-type': gettext('Route Type'),
+    vni: gettext('VNI'),
+    'ip-address-prefix-list': gettext('IPv4 (prefix-list)'),
+    'ip6-address-prefix-list': gettext('IPv6 (prefix-list)'),
+    'ip-next-hop-prefix-list': gettext('IPv4 next-hop (prefix-list)'),
+    'ip6-next-hop-prefix-list': gettext('IPv6 next-hop (prefix-list)'),
+    'ip-next-hop-address': gettext('IPv4 next-hop'),
+    'ip6-next-hop-address': gettext('IPv6 next-hop'),
+    metric: gettext('Metric'),
+    'local-preference': gettext('Local Preference'),
+    peer: gettext('Peer'),
+};
+
+Ext.define('PVE.sdn.RouteMapMatchField', {
+    extend: 'Ext.grid.Panel',
+    mixins: ['Ext.form.field.Field'],
+    alias: 'widget.pveSdnRouteMapMatchField',
+
+    emptyText: gettext('No match actions configured.'),
+
+    isCreate: false,
+
+    store: {
+        model: 'PVE.sdn.RouteMapMatch',
+    },
+
+    columns: [
+        {
+            header: gettext('Property'),
+            xtype: 'widgetcolumn',
+            flex: 1,
+            widget: {
+                xtype: 'proxmoxKVComboBox',
+                comboItems: Object.entries(ROUTE_MAP_MATCH_ACTION_LABELS),
+                allowBlank: false,
+                deleteEmpty: false,
+                bind: {
+                    value: '{record.key}',
+                },
+                listeners: {
+                    select: function (_this, newValue) {
+                        let me = this;
+                        me.getWidgetRecord().set('key', newValue.id);
+                    },
+                },
+            },
+        },
+        {
+            header: gettext('Value'),
+            flex: 1,
+            xtype: 'widgetcolumn',
+            widget: {
+                xtype: 'pveSdnRouteMapMatchValueField',
+                bind: {
+                    key: '{record.key}',
+                    record: '{record}',
+                },
+            },
+        },
+        {
+            width: 20,
+            xtype: 'actioncolumn',
+            items: [
+                {
+                    tooltip: gettext('Delete'),
+                    handler: 'deleteMatch',
+                    iconCls: 'fa critical fa-trash-o',
+                },
+            ],
+        },
+    ],
+
+    initComponent: function () {
+        let me = this;
+        me.callParent();
+
+        me.getStore().on('datachanged', function () {
+            me.fireEvent('validitychange');
+            me.fireEvent('dirtychange');
+        });
+    },
+
+    getValue: function () {
+        let me = this;
+
+        return me
+            .getStore()
+            .getData()
+            .items.map((item) => {
+                let data = item.data;
+                delete data.id;
+
+                if (!data.value) {
+                    delete data.value;
+                }
+
+                return PVE.Parser.printPropertyString(data);
+            });
+    },
+
+    setValue: function (value) {
+        let me = this;
+        me.getStore().setData(value.map(PVE.Parser.parsePropertyString));
+    },
+
+    getSubmitData: function () {
+        let me = this;
+
+        let value = me.getValue();
+        if (value.length === 0) {
+            return {
+                delete: [me.getName()],
+            };
+        }
+
+        return {
+            [me.getName()]: value,
+        };
+    },
+
+    tbar: [
+        {
+            xtype: 'button',
+            text: gettext('Add'),
+            handler: 'addEntry',
+        },
+    ],
+
+    controller: {
+        addEntry: function () {
+            let me = this;
+            me.getView().getStore().add({
+                key: null,
+                value: null,
+            });
+        },
+        deleteMatch: function (_table, _rI, _cI, _item, _e, record) {
+            let me = this;
+            me.getView().getStore().remove(record);
+        },
+    },
+});
+
+Ext.define('PVE.sdn.RouteMapExitActionField', {
+    extend: 'Ext.container.Container',
+    mixins: ['Ext.form.field.Field'],
+    alias: 'widget.pveSdnRouteMapExitActionField',
+
+    layout: 'hbox',
+
+    viewModel: {
+        data: {
+            exitAction: null,
+        },
+    },
+
+    items: [
+        {
+            xtype: 'proxmoxKVComboBox',
+            flex: 1,
+            fieldLabel: gettext('Exit Policy'),
+            bind: {
+                value: '{exitAction.key}',
+            },
+            value: '__default__',
+            comboItems: [
+                ['__default__', Proxmox.Utils.defaultText + gettext('(exit)')],
+                ['on-match-next', gettext('On match next')],
+            ],
+            deleteEmpty: false,
+            editable: false,
+            isFormField: false,
+            listeners: {
+                select: 'onSelect',
+            },
+        },
+    ],
+
+    controller: {
+        onSelect: function () {
+            let me = this;
+            me.getView().fireEvent('dirtychange');
+        },
+    },
+
+    getValue: function () {
+        let me = this;
+
+        let exitAction = me.getViewModel().get('exitAction');
+
+        if (!exitAction?.key || exitAction.key === '__default__') {
+            return null;
+        }
+
+        return PVE.Parser.printPropertyString(exitAction);
+    },
+
+    setValue: function (value) {
+        let me = this;
+
+        let exitAction = PVE.Parser.parsePropertyString(value);
+
+        me.getViewModel().set('exitAction', exitAction);
+        me.resetOriginalValue();
+    },
+
+    getSubmitData: function () {
+        let me = this;
+
+        let value = me.getValue();
+
+        if (!value) {
+            return {
+                delete: [me.getName()],
+            };
+        }
+
+        return {
+            [me.getName()]: value,
+        };
+    },
+});
+
+Ext.define('PVE.sdn.EditRouteMapEntryWindow', {
+    extend: 'Proxmox.window.Edit',
+    subject: gettext('Route Map Entry'),
+
+    initComponent: function () {
+        let me = this;
+        me.method = me.isCreate ? 'POST' : 'PUT';
+
+        me.callParent();
+    },
+
+    loadUrl: function () {
+        let me = this;
+        return `/api2/extjs/cluster/sdn/route-maps/${me.getRouteMapId()}/${me.getOrder()}`;
+    },
+
+    submitUrl: function () {
+        let me = this;
+
+        if (me.isCreate) {
+            return '/api2/extjs/cluster/sdn/route-maps';
+        } else {
+            return `/api2/extjs/cluster/sdn/route-maps/${me.getRouteMapId()}/${me.getOrder()}`;
+        }
+    },
+
+    width: 600,
+
+    viewModel: {
+        formulas: {
+            routeMapId: function (get) {
+                let me = this;
+                return me.getView().getRouteMapId();
+            },
+            order: function (get) {
+                let me = this;
+                return me.getView().getOrder();
+            },
+        },
+    },
+
+    config: {
+        routeMapId: null,
+        order: null,
+    },
+
+    isCreate: false,
+
+    items: [
+        {
+            xtype: 'pveSDNRouteMapSelector',
+            name: 'route-map-id',
+            fieldLabel: gettext('Route Map ID'),
+            editable: true,
+            notFoundIsValid: true,
+            bind: {
+                disabled: '{routeMapId}',
+            },
+        },
+        {
+            xtype: 'proxmoxtextfield',
+            name: 'order',
+            fieldLabel: gettext('Order'),
+            bind: {
+                disabled: '{order}',
+            },
+        },
+        {
+            xtype: 'proxmoxKVComboBox',
+            fieldLabel: gettext('Action'),
+            name: 'action',
+            comboItems: [
+                ['permit', gettext('Permit')],
+                ['deny', gettext('Deny')],
+            ],
+            allowBlank: false,
+        },
+        {
+            xtype: 'fieldcontainer',
+            fieldLabel: gettext('Match'),
+            items: [
+                {
+                    xtype: 'pveSdnRouteMapMatchField',
+                    name: 'match',
+                },
+            ],
+        },
+        {
+            xtype: 'fieldcontainer',
+            fieldLabel: gettext('Set'),
+            items: [
+                {
+                    xtype: 'pveSdnRouteMapSetField',
+                    name: 'set',
+                },
+            ],
+        },
+        {
+            xtype: 'pveSDNRouteMapSelector',
+            fieldLabel: gettext('Call'),
+            name: 'call',
+            deleteEmpty: true,
+            skipEmptyText: true,
+        },
+        {
+            xtype: 'pveSdnRouteMapExitActionField',
+            fieldLabel: gettext('Exit Policy'),
+            name: 'exit-action',
+        },
+    ],
+});
+
+Ext.define('PVE.sdn.RouteMapPanel', {
+    extend: 'Ext.grid.Panel',
+    alias: ['widget.pveSDNRouteMaps'],
+
+    emptyText: gettext('No route maps configured.'),
+
+    store: {
+        autoLoad: true,
+        model: 'PVE.sdn.RouteMapEntry',
+        proxy: {
+            type: 'proxmox',
+            url: '/api2/extjs/cluster/sdn/route-maps?pending=1',
+        },
+        sorters: [
+            {
+                property: 'route-map-id',
+                direction: 'ASC',
+            },
+            {
+                property: 'order',
+                direction: 'ASC',
+            },
+        ],
+    },
+
+    viewModel: {
+        formulas: {
+            selection: function (get) {
+                let me = this;
+
+                let selection = me.getView().getSelection();
+                return selection.length > 0 ? selection[0] : null;
+            },
+        },
+    },
+
+    listeners: {
+        itemdblclick: 'editRouteMapEntry',
+    },
+
+    controller: {
+        reload: function () {
+            let me = this;
+            me.getView().getStore().load();
+        },
+        addRouteMapEntry: function () {
+            let me = this;
+
+            Ext.create('PVE.sdn.EditRouteMapEntryWindow', {
+                autoShow: true,
+                isCreate: true,
+                listeners: {
+                    close: function () {
+                        me.reload();
+                    },
+                },
+            });
+        },
+        removeRouteMapEntry: function () {
+            let me = this;
+
+            let entry = me.getView().getSelection()[0];
+
+            if (!entry) {
+                console.warn('no route map entry selected!');
+                return;
+            }
+
+            Ext.Msg.show({
+                title: gettext('Confirm'),
+                icon: Ext.Msg.WARNING,
+                message: gettext('Remove route map entry?'),
+                buttons: Ext.Msg.YESNO,
+                defaultFocus: 'no',
+                callback: function (btn) {
+                    if (btn !== 'yes') {
+                        return;
+                    }
+
+                    Proxmox.Async.api2({
+                        url: `/api2/extjs/cluster/sdn/route-maps/${entry.getRouteMapId()}/${entry.getOrder()}`,
+                        method: 'DELETE',
+                    })
+                        .catch(Proxmox.Utils.alertResponseFailure)
+                        .finally(() => {
+                            me.reload();
+                        });
+                },
+            });
+        },
+        editRouteMapEntry: function () {
+            let me = this;
+
+            let entry = me.getView().getSelection()[0];
+
+            if (!entry) {
+                console.warn('no route map entry selected!');
+                return;
+            }
+
+            Ext.create('PVE.sdn.EditRouteMapEntryWindow', {
+                autoShow: true,
+                autoLoad: true,
+                isCreate: false,
+                routeMapId: entry.getRouteMapId(),
+                order: entry.getOrder(),
+                listeners: {
+                    close: function () {
+                        me.reload();
+                    },
+                },
+            });
+        },
+    },
+
+    tbar: [
+        {
+            text: gettext('Add'),
+            xtype: 'button',
+            handler: 'addRouteMapEntry',
+        },
+        {
+            text: gettext('Edit'),
+            xtype: 'proxmoxButton',
+            handler: 'editRouteMapEntry',
+            bind: {
+                disabled: '{!selection}',
+            },
+        },
+        {
+            text: gettext('Remove'),
+            xtype: 'proxmoxButton',
+            handler: 'removeRouteMapEntry',
+            bind: {
+                disabled: '{!selection}',
+            },
+        },
+        {
+            text: gettext('Reload'),
+            xtype: 'button',
+            handler: 'reload',
+        },
+    ],
+
+    columns: [
+        {
+            text: gettext('Name'),
+            dataIndex: 'route-map-id',
+            flex: 1,
+            renderer: function (value, metaData, rec) {
+                return PVE.Utils.render_sdn_pending(rec, value, 'route-map-id', 1);
+            },
+        },
+        {
+            text: gettext('Order'),
+            dataIndex: 'order',
+            width: 50,
+            renderer: function (value, metaData, rec) {
+                return PVE.Utils.render_sdn_pending(rec, value, 'order', 1);
+            },
+        },
+        {
+            text: gettext('Action'),
+            dataIndex: 'action',
+            width: 80,
+            renderer: function (value, metaData, rec) {
+                return PVE.Utils.render_sdn_pending(rec, value, 'action', 1);
+            },
+        },
+        {
+            text: gettext('Match'),
+            dataIndex: 'match',
+            flex: 1,
+            renderer: function (value, metaData, rec) {
+                let actions = rec.data.pending?.match ?? rec.data.match ?? [];
+
+                return actions
+                    .map(PVE.Parser.parsePropertyString)
+                    .map((match) => {
+                        let label = ROUTE_MAP_MATCH_ACTION_LABELS[match.key] ?? match.key;
+                        let value = match.value ? `: ${match.value}` : '';
+                        return Ext.htmlEncode(`${label}${value}`);
+                    })
+                    .join('<br>');
+            },
+        },
+        {
+            text: gettext('Set'),
+            dataIndex: 'set',
+            flex: 1,
+            renderer: function (value, metaData, rec) {
+                let actions = rec.data.pending?.set ?? rec.data.set ?? [];
+
+                return actions
+                    .map(PVE.Parser.parsePropertyString)
+                    .map((match) => {
+                        let label = ROUTE_MAP_SET_ACTION_LABELS[match.key] ?? match.key;
+                        let value = match.value ? `: ${match.value}` : '';
+                        return Ext.htmlEncode(`${label}${value}`);
+                    })
+                    .join('<br>');
+            },
+        },
+        {
+            text: gettext('Call'),
+            dataIndex: 'call',
+            flex: 1,
+            renderer: function (value, metaData, rec) {
+                return PVE.Utils.render_sdn_pending(rec, value, 'call', 1);
+            },
+        },
+        {
+            header: gettext('Exit Policy'),
+            width: 100,
+            dataIndex: 'exit-action',
+            renderer: function (value, metaData, rec) {
+                let exitAction = rec.data.pending?.['exit-action'] ?? rec.data['exit-action'];
+
+                if (exitAction) {
+                    let parsedExitAction = PVE.Parser.parsePropertyString(exitAction);
+                    return Ext.htmlEncode(`${parsedExitAction.key}`);
+                }
+            },
+        },
+        {
+            header: gettext('State'),
+            width: 100,
+            dataIndex: 'state',
+            renderer: function (value, metaData, rec) {
+                return PVE.Utils.render_sdn_pending_state(rec, value);
+            },
+        },
+    ],
+});
-- 
2.47.3





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

* [PATCH pve-manager v6 13/24] ui: sdn: bgp controller: allow configuring route maps
  2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
                   ` (11 preceding siblings ...)
  2026-05-08 16:31 ` [PATCH pve-manager v6 12/24] ui: sdn: add panel for managing route map entries Stefan Hanreich
@ 2026-05-08 16:31 ` Stefan Hanreich
  2026-05-08 16:31 ` [PATCH pve-manager v6 14/24] ui: sdn: evpn " Stefan Hanreich
                   ` (10 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2026-05-08 16:31 UTC (permalink / raw)
  To: pve-devel; +Cc: Thomas Lamprecht

Expose the option to apply user-defined route maps to the BGP
controller by utilizing the new route map selector component.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Link: https://lore.proxmox.com/20260505153720.412180-44-s.hanreich@proxmox.com
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
---
 www/manager6/sdn/controllers/BgpEdit.js | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/www/manager6/sdn/controllers/BgpEdit.js b/www/manager6/sdn/controllers/BgpEdit.js
index 0bb53cd2b..09b001f5f 100644
--- a/www/manager6/sdn/controllers/BgpEdit.js
+++ b/www/manager6/sdn/controllers/BgpEdit.js
@@ -73,6 +73,20 @@ Ext.define('PVE.sdn.controllers.BgpInputPanel', {
                 checked: false,
                 fieldLabel: 'bgp-multipath-as-path-relax',
             },
+            {
+                xtype: 'pveSDNRouteMapSelector',
+                name: 'route-map-in',
+                fieldLabel: gettext('Incoming Route Map'),
+                deleteEmpty: true,
+                skipEmptyText: true,
+            },
+            {
+                xtype: 'pveSDNRouteMapSelector',
+                name: 'route-map-out',
+                fieldLabel: gettext('Outgoing Route Map'),
+                deleteEmpty: true,
+                skipEmptyText: true,
+            },
         ];
 
         me.callParent();
-- 
2.47.3





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

* [PATCH pve-manager v6 14/24] ui: sdn: evpn controller: allow configuring route maps
  2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
                   ` (12 preceding siblings ...)
  2026-05-08 16:31 ` [PATCH pve-manager v6 13/24] ui: sdn: bgp controller: allow configuring route maps Stefan Hanreich
@ 2026-05-08 16:31 ` Stefan Hanreich
  2026-05-08 16:31 ` [PATCH pve-manager v6 15/24] ui: sdn: openfabric: add route filter Stefan Hanreich
                   ` (9 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2026-05-08 16:31 UTC (permalink / raw)
  To: pve-devel; +Cc: Thomas Lamprecht

Expose the option to apply user-defined route maps to the BGP
controller by utilizing the new route map selector component.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Link: https://lore.proxmox.com/20260505153720.412180-45-s.hanreich@proxmox.com
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
---
 www/manager6/sdn/controllers/EvpnEdit.js | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/www/manager6/sdn/controllers/EvpnEdit.js b/www/manager6/sdn/controllers/EvpnEdit.js
index 67c41f476..01b85b541 100644
--- a/www/manager6/sdn/controllers/EvpnEdit.js
+++ b/www/manager6/sdn/controllers/EvpnEdit.js
@@ -63,6 +63,23 @@ Ext.define('PVE.sdn.controllers.EvpnInputPanel', {
             },
         ];
 
+        me.advancedItems = [
+            {
+                xtype: 'pveSDNRouteMapSelector',
+                name: 'route-map-in',
+                fieldLabel: gettext('Incoming Route Map'),
+                deleteEmpty: true,
+                skipEmptyText: true,
+            },
+            {
+                xtype: 'pveSDNRouteMapSelector',
+                name: 'route-map-out',
+                fieldLabel: gettext('Outgoing Route Map'),
+                deleteEmpty: true,
+                skipEmptyText: true,
+            },
+        ];
+
         me.callParent();
     },
 });
-- 
2.47.3





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

* [PATCH pve-manager v6 15/24] ui: sdn: openfabric: add route filter
  2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
                   ` (13 preceding siblings ...)
  2026-05-08 16:31 ` [PATCH pve-manager v6 14/24] ui: sdn: evpn " Stefan Hanreich
@ 2026-05-08 16:31 ` Stefan Hanreich
  2026-05-08 16:31 ` [PATCH pve-manager v6 16/24] ui: sdn: ospf: add route filter setting Stefan Hanreich
                   ` (8 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2026-05-08 16:31 UTC (permalink / raw)
  To: pve-devel; +Cc: Thomas Lamprecht

Expose the Route Filter option for openfabric by utilizing the new
prefix list selector component.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Link: https://lore.proxmox.com/20260505153720.412180-46-s.hanreich@proxmox.com
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
---
 www/manager6/sdn/fabrics/openfabric/FabricEdit.js | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/www/manager6/sdn/fabrics/openfabric/FabricEdit.js b/www/manager6/sdn/fabrics/openfabric/FabricEdit.js
index 14b71fae2..77851a7df 100644
--- a/www/manager6/sdn/fabrics/openfabric/FabricEdit.js
+++ b/www/manager6/sdn/fabrics/openfabric/FabricEdit.js
@@ -68,5 +68,13 @@ Ext.define('PVE.sdn.Fabric.OpenFabric.Fabric.Edit', {
                 deleteEmpty: '{!isCreate}',
             },
         },
+        {
+            xtype: 'pveSDNPrefixListSelector',
+            name: 'route_filter',
+            fieldLabel: gettext('Route Filter'),
+            emptyText: gettext('IP Prefixes'),
+            deleteEmpty: true,
+            skipEmptyText: true,
+        },
     ],
 });
-- 
2.47.3





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

* [PATCH pve-manager v6 16/24] ui: sdn: ospf: add route filter setting
  2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
                   ` (14 preceding siblings ...)
  2026-05-08 16:31 ` [PATCH pve-manager v6 15/24] ui: sdn: openfabric: add route filter Stefan Hanreich
@ 2026-05-08 16:31 ` Stefan Hanreich
  2026-05-08 16:31 ` [PATCH pve-manager v6 17/24] ui: sdn: prefix list: add missing subjects Stefan Hanreich
                   ` (7 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2026-05-08 16:31 UTC (permalink / raw)
  To: pve-devel; +Cc: Thomas Lamprecht

Expose the Route Filter option for ospf by utilizing the new prefix
list selector component.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Link: https://lore.proxmox.com/20260505153720.412180-47-s.hanreich@proxmox.com
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
---
 www/manager6/sdn/fabrics/ospf/FabricEdit.js | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/www/manager6/sdn/fabrics/ospf/FabricEdit.js b/www/manager6/sdn/fabrics/ospf/FabricEdit.js
index 4c4e17c99..d0de87cff 100644
--- a/www/manager6/sdn/fabrics/ospf/FabricEdit.js
+++ b/www/manager6/sdn/fabrics/ospf/FabricEdit.js
@@ -17,5 +17,13 @@ Ext.define('PVE.sdn.Fabric.Ospf.Fabric.Edit', {
             emptyText: '0',
             allowBlank: false,
         },
+        {
+            xtype: 'pveSDNPrefixListSelector',
+            name: 'route_filter',
+            fieldLabel: gettext('Route Filter'),
+            emptyText: gettext('IP Prefixes'),
+            deleteEmpty: true,
+            skipEmptyText: true,
+        },
     ],
 });
-- 
2.47.3





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

* [PATCH pve-manager v6 17/24] ui: sdn: prefix list: add missing subjects
  2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
                   ` (15 preceding siblings ...)
  2026-05-08 16:31 ` [PATCH pve-manager v6 16/24] ui: sdn: ospf: add route filter setting Stefan Hanreich
@ 2026-05-08 16:31 ` Stefan Hanreich
  2026-05-08 16:31 ` [PATCH pve-manager v6 18/24] sdn: do not fail rendering record data if pending property is missing Stefan Hanreich
                   ` (6 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2026-05-08 16:31 UTC (permalink / raw)
  To: pve-devel

From: Dominik Csapak <d.csapak@proxmox.com>

so the title of the edit window is not empty on add/edit

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 www/manager6/sdn/PrefixListPanel.js | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/www/manager6/sdn/PrefixListPanel.js b/www/manager6/sdn/PrefixListPanel.js
index 7152058e7..cdb860f4f 100644
--- a/www/manager6/sdn/PrefixListPanel.js
+++ b/www/manager6/sdn/PrefixListPanel.js
@@ -16,6 +16,8 @@ Ext.define('PVE.sdn.PrefixListEntry', {
 Ext.define('PVE.sdn.EditPrefixListWindow', {
     extend: 'Proxmox.window.Edit',
 
+    subject: gettext('Prefix List'),
+
     url: '/cluster/sdn/prefix-lists',
 
     config: {
@@ -44,6 +46,8 @@ Ext.define('PVE.sdn.EditPrefixListWindow', {
 Ext.define('PVE.sdn.EditPrefixListEntryWindow', {
     extend: 'Proxmox.window.Edit',
 
+    subject: gettext('Prefix List Entry'),
+
     url: '/cluster/sdn/prefix-lists',
 
     config: {
-- 
2.47.3





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

* [PATCH pve-manager v6 18/24] sdn: do not fail rendering record data if pending property is missing
  2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
                   ` (16 preceding siblings ...)
  2026-05-08 16:31 ` [PATCH pve-manager v6 17/24] ui: sdn: prefix list: add missing subjects Stefan Hanreich
@ 2026-05-08 16:31 ` Stefan Hanreich
  2026-05-08 16:31 ` [PATCH pve-manager v6 19/24] ui: sdn: prefix list: adapt to changed api structure Stefan Hanreich
                   ` (5 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2026-05-08 16:31 UTC (permalink / raw)
  To: pve-devel

The render helpers for the SDN pending status fail on rendering the
values of the columns if the pending property does not exist. With the
introduction of the prefix lists endpoint, where the API returns the
id and the state of the prefix list only, but not the pending changes,
the function fails silently and the error is swallowed by ExtJS.
Handle this situation more gracefully by adding null checks when
accessing the properties of the passed record.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 www/manager6/Utils.js | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index be95d2167..220a033d3 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -193,13 +193,14 @@ Ext.define('PVE.Utils', {
                 } else {
                     return `<span style="text-decoration: line-through;">${Ext.htmlEncode(value)}</span>`;
                 }
-            } else if (rec.data.pending[key] !== undefined && rec.data.pending[key] !== null) {
+            } else if (rec.data?.pending?.[key] !== undefined && rec.data?.pending?.[key] !== null) {
                 if (rec.data.pending[key] === 'deleted') {
                     return ' ';
                 } else {
                     return Ext.htmlEncode(rec.data.pending[key]);
                 }
             }
+
             return Ext.htmlEncode(value);
         },
 
@@ -216,7 +217,7 @@ Ext.define('PVE.Utils', {
 
             let tip = gettext('Pending Changes') + ': <br>';
 
-            for (const [key, keyvalue] of Object.entries(rec.data.pending)) {
+            for (const [key, keyvalue] of Object.entries(rec.data.pending ?? {})) {
                 if (
                     (rec.data[key] !== undefined && rec.data.pending[key] !== rec.data[key]) ||
                     rec.data[key] === undefined
-- 
2.47.3





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

* [PATCH pve-manager v6 19/24] ui: sdn: prefix list: adapt to changed api structure
  2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
                   ` (17 preceding siblings ...)
  2026-05-08 16:31 ` [PATCH pve-manager v6 18/24] sdn: do not fail rendering record data if pending property is missing Stefan Hanreich
@ 2026-05-08 16:31 ` Stefan Hanreich
  2026-05-08 16:31 ` [PATCH pve-manager v6 20/24] ui: sdn: route maps: adapt to new route map " Stefan Hanreich
                   ` (4 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2026-05-08 16:31 UTC (permalink / raw)
  To: pve-devel

From: Dominik Csapak <d.csapak@proxmox.com>

instead of having to parse out the prefix list entries from the overall
prefix list GET api call, the entries are now queried below:

/sdn/cluster/prefix-list/{id}/entries

so the entries grid can be simplified to use it's own store + url.
This makes it possible to move the stores out of the viewmodel and into
the respective grid views.

Also use proxmoxStdRemoveButtons here to simplify the removal code.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
[SH: fixed wrong url in edit window, made sequence number optional and
added empty text]
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 www/manager6/sdn/PrefixListPanel.js | 279 ++++++++++------------------
 1 file changed, 96 insertions(+), 183 deletions(-)

diff --git a/www/manager6/sdn/PrefixListPanel.js b/www/manager6/sdn/PrefixListPanel.js
index cdb860f4f..0e5cc11b8 100644
--- a/www/manager6/sdn/PrefixListPanel.js
+++ b/www/manager6/sdn/PrefixListPanel.js
@@ -1,16 +1,11 @@
 Ext.define('PVE.sdn.PrefixList', {
     extend: 'Ext.data.Model',
-    fields: ['id', 'entries', 'pending'],
-
-    getId: function () {
-        let me = this;
-        return me.data.pending?.[me.idProperty] ?? me.data[me.idProperty];
-    },
+    fields: ['id', 'state'],
 });
 
 Ext.define('PVE.sdn.PrefixListEntry', {
     extend: 'Ext.data.Model',
-    fields: ['id', 'action', 'prefix', 'le', 'ge', 'pending'],
+    fields: ['id', 'action', 'seq', 'prefix', 'le', 'ge', 'pending'],
 });
 
 Ext.define('PVE.sdn.EditPrefixListWindow', {
@@ -45,10 +40,11 @@ Ext.define('PVE.sdn.EditPrefixListWindow', {
 
 Ext.define('PVE.sdn.EditPrefixListEntryWindow', {
     extend: 'Proxmox.window.Edit',
+    mixins: ['Proxmox.Mixin.CBind'],
 
     subject: gettext('Prefix List Entry'),
 
-    url: '/cluster/sdn/prefix-lists',
+    baseUrl: '/cluster/sdn/prefix-lists',
 
     config: {
         entry: null,
@@ -57,6 +53,15 @@ Ext.define('PVE.sdn.EditPrefixListEntryWindow', {
     isCreate: false,
 
     items: [
+        {
+            xtype: 'proxmoxintegerfield',
+            name: 'seq',
+            fieldLabel: gettext('Sequence Nr.'),
+            emptyText: gettext('autogenerated'),
+            cbind: {
+                deleteEmpty: '{!isCreate}',
+            }
+        },
         {
             xtype: 'proxmoxKVComboBox',
             fieldLabel: gettext('Action'),
@@ -88,6 +93,17 @@ Ext.define('PVE.sdn.EditPrefixListEntryWindow', {
     initComponent: function () {
         let me = this;
         me.method = me.isCreate ? 'POST' : 'PUT';
+
+        if (!me.prefixList) {
+            throw new 'no prefixList given'();
+        }
+
+        if (me.entry) {
+            me.url = `${me.baseUrl}/${me.prefixList}/entries/${me.entry.seq}`;
+        } else {
+            me.url = `${me.baseUrl}/${me.prefixList}/entries`;
+        }
+
         me.callParent();
 
         me.setValues(me.getEntry());
@@ -108,19 +124,28 @@ Ext.define('PVE.sdn.PrefixListView', {
         },
         {
             text: gettext('Remove'),
-            xtype: 'button',
-            handler: 'removePrefixList',
-            bind: {
-                disabled: '{!prefixListGrid.selection}',
-            },
+            xtype: 'proxmoxStdRemoveButton',
+            baseurl: '/cluster/sdn/prefix-lists/',
+            dangerous: true,
+            callback: 'reloadPrefixList',
         },
+        '->',
         {
             text: gettext('Reload'),
             xtype: 'button',
-            handler: 'reload',
+            handler: 'reloadPrefixList',
         },
     ],
 
+    store: {
+        autoLoad: true,
+        model: 'PVE.sdn.PrefixList',
+        proxy: {
+            type: 'proxmox',
+            url: '/api2/json/cluster/sdn/prefix-lists?pending=1',
+        },
+    },
+
     columns: [
         {
             text: gettext('Name'),
@@ -139,6 +164,12 @@ Ext.define('PVE.sdn.PrefixListView', {
             },
         },
     ],
+
+    initComponent: function () {
+        let me = this;
+        me.callParent();
+        Proxmox.Utils.monStoreErrors(me, me.getStore());
+    },
 });
 
 Ext.define('PVE.sdn.PrefixListEntriesView', {
@@ -151,30 +182,22 @@ Ext.define('PVE.sdn.PrefixListEntriesView', {
         prefixList: null,
     },
 
-    viewConfig: {
-        plugins: [
-            {
-                ptype: 'gridviewdragdrop',
-            },
-        ],
-    },
-
     listeners: {
-        drop: 'saveEntries',
         itemdblclick: 'editPrefixListEntry',
     },
 
+    store: {
+        model: 'PVE.sdn.PrefixListEntry',
+        proxy: {
+            type: 'proxmox',
+        },
+    },
+
     columns: [
         {
-            width: 40,
-            resizable: false,
-            sortable: false,
-            hideable: false,
-            menuDisabled: true,
-            renderer: function (value, metaData, record, rowIdx, colIdx) {
-                metaData.tdCls = Ext.baseCSSPrefix + 'grid-cell-special';
-                return "<i class='pve-grid-fa fa fa-fw fa-reorder cursor-move'></i>";
-            },
+            text: gettext('Sequence Nr.'),
+            dataIndex: 'seq',
+            flex: 1,
         },
         {
             text: gettext('Action'),
@@ -209,21 +232,32 @@ Ext.define('PVE.sdn.PrefixListEntriesView', {
         },
         {
             text: gettext('Edit'),
-            xtype: 'button',
+            xtype: 'proxmoxButton',
+            disabled: true,
             handler: 'editPrefixListEntry',
-            bind: {
-                disabled: '{!prefixListEntriesGrid.selection}',
-            },
         },
         {
             text: gettext('Remove'),
-            xtype: 'button',
-            handler: 'removePrefixListEntry',
-            bind: {
-                disabled: '{!prefixListEntriesGrid.selection}',
+            xtype: 'proxmoxStdRemoveButton',
+            customConfirmationMessage: gettext(
+                'Are you sure you want to remove entry with sequence {0}',
+            ),
+            getRecordName: (rec) => rec.data.seq,
+            getUrl: function (rec) {
+                let grid = this.up('grid');
+                let prefixList = grid.prefixList;
+                let id = prefixList.getId();
+                let seq = rec.data.seq;
+                return `/cluster/sdn/prefix-lists/${id}/entries/${seq}`;
             },
         },
     ],
+
+    initComponent: function () {
+        let me = this;
+        me.callParent();
+        Proxmox.Utils.monStoreErrors(me, me.getStore());
+    },
 });
 
 Ext.define('PVE.sdn.PrefixListPanel', {
@@ -233,30 +267,6 @@ Ext.define('PVE.sdn.PrefixListPanel', {
     emptyText: gettext('No prefix list configured'),
 
     viewModel: {
-        stores: {
-            prefixLists: {
-                autoLoad: true,
-                model: 'PVE.sdn.PrefixList',
-                proxy: {
-                    type: 'proxmox',
-                    url: '/api2/json/cluster/sdn/prefix-lists?pending=1',
-                },
-            },
-            prefixListEntries: {
-                model: 'PVE.sdn.PrefixListEntry',
-                proxy: {
-                    type: 'proxmox',
-                    reader: {
-                        transform: {
-                            fn: function (response) {
-                                let entries = response.data.entries ?? [];
-                                return entries.map(PVE.Parser.parsePropertyString);
-                            },
-                        },
-                    },
-                },
-            },
-        },
         formulas: {
             entryGridEmptyText: function (get) {
                 let selection = get('prefixListGrid.selection');
@@ -269,64 +279,24 @@ Ext.define('PVE.sdn.PrefixListPanel', {
     },
 
     controller: {
-        reload: function () {
+        reloadPrefixList: function () {
             let me = this;
-
-            let prefixList = me.getViewModel().get('prefixListGrid.selection');
-
-            me.getViewModel()
-                .getStore('prefixLists')
-                .load((records, _operation, success) => {
-                    if (!success || !prefixList) {
-                        return;
-                    }
-
-                    let newPrefixList = records.find((record) => {
-                        return record.getId() === prefixList.getId();
-                    });
-
-                    me.lookupReference('prefixListGrid').setSelection(newPrefixList);
-                });
+            me.lookup('prefixListGrid').getStore().load();
+            // reset entries grid
+            let entriesGrid = me.lookup('prefixListEntriesGrid');
+            entriesGrid.getStore().setData([]);
+            Proxmox.Utils.setErrorMask(entriesGrid, false);
         },
-        saveEntries: function () {
+        reloadPrefixEntries: function () {
             let me = this;
-
-            let prefixList = me.getViewModel().get('prefixListGrid.selection');
-
-            let entries = me
-                .getViewModel()
-                .getStore('prefixListEntries')
-                .getData()
-                .items.map((item) => {
-                    let data = item.data;
-                    delete data.id;
-
-                    return PVE.Parser.printPropertyString(data);
-                });
-
-            let params = {};
-
-            if (entries.length > 0) {
-                params.entries = entries;
-            } else {
-                params = { delete: ['entries'] };
-            }
-
-            Proxmox.Async.api2({
-                url: `/api2/extjs/cluster/sdn/prefix-lists/${prefixList.getId()}`,
-                params,
-                method: 'PUT',
-            })
-                .catch(Proxmox.Utils.alertResponseFailure)
-                .finally(() => {
-                    me.reload(prefixList);
-                });
+            me.lookup('prefixListEntriesGrid').getStore().load();
         },
         selectPrefixList: function (gridPanel, record, index, options) {
             let me = this;
 
-            let url = `/api2/extjs/cluster/sdn/prefix-lists/${record.getId()}`;
-            let entryStore = me.getViewModel().getStore('prefixListEntries');
+            let prefixEntriesGrid = me.lookup('prefixListEntriesGrid');
+            let entryStore = prefixEntriesGrid.getStore();
+            let url = `/api2/json/cluster/sdn/prefix-lists/${record.getId()}/entries`;
 
             entryStore.getProxy().setUrl(url);
             entryStore.load();
@@ -338,56 +308,28 @@ Ext.define('PVE.sdn.PrefixListPanel', {
                 autoShow: true,
                 isCreate: true,
                 listeners: {
-                    close: () => me.reload(),
+                    close: () => me.reloadPrefixList(),
                 },
             });
         },
-        removePrefixList: function () {
+        addPrefixListEntry: function () {
             let me = this;
             let prefixList = me.getViewModel().get('prefixListGrid.selection');
 
-            Ext.Msg.show({
-                title: gettext('Confirm'),
-                icon: Ext.Msg.WARNING,
-                message: Ext.String.format(gettext('Remove prefix list "{0}"?'), prefixList.getId()),
-                buttons: Ext.Msg.YESNO,
-                defaultFocus: 'no',
-                callback: function (btn) {
-                    if (btn !== 'yes') {
-                        return;
-                    }
-
-                    Proxmox.Async.api2({
-                        url: `/api2/extjs/cluster/sdn/prefix-lists/${prefixList.getId()}`,
-                        method: 'DELETE',
-                    })
-                        .catch(Proxmox.Utils.alertResponseFailure)
-                        .finally(() => {
-                            me.reload(prefixList);
-                        });
-                },
-            });
-        },
-        addPrefixListEntry: function () {
-            let panel = this;
-
             Ext.create('PVE.sdn.EditPrefixListEntryWindow', {
                 autoShow: true,
                 isCreate: true,
-                submit: function () {
-                    let me = this;
-
-                    panel.getViewModel().getStore('prefixListEntries').add(me.getValues());
-                    panel.saveEntries();
-
-                    me.close();
+                prefixList: prefixList.getId(),
+                listeners: {
+                    close: () => me.reloadPrefixEntries(),
                 },
             });
         },
         editPrefixListEntry: function () {
-            let panel = this;
+            let me = this;
 
-            let entry = panel.getViewModel().get('prefixListEntriesGrid.selection');
+            let entry = me.getViewModel().get('prefixListEntriesGrid.selection');
+            let prefixList = me.getViewModel().get('prefixListGrid.selection');
 
             if (!entry) {
                 console.warn('no prefix list entry selected!');
@@ -397,35 +339,10 @@ Ext.define('PVE.sdn.PrefixListPanel', {
             Ext.create('PVE.sdn.EditPrefixListEntryWindow', {
                 autoShow: true,
                 isCreate: false,
+                prefixList: prefixList.getId(),
                 entry: entry.data,
-                submit: function () {
-                    let me = this;
-                    entry.set(me.getValues());
-
-                    panel.saveEntries();
-
-                    me.close();
-                },
-            });
-        },
-        removePrefixListEntry: function () {
-            let me = this;
-
-            let entry = me.getViewModel().get('prefixListEntriesGrid.selection');
-
-            Ext.Msg.show({
-                title: gettext('Confirm'),
-                icon: Ext.Msg.WARNING,
-                message: gettext('Remove prefix list entry?'),
-                buttons: Ext.Msg.YESNO,
-                defaultFocus: 'no',
-                callback: function (btn) {
-                    if (btn !== 'yes') {
-                        return;
-                    }
-
-                    me.getViewModel().getStore('prefixListEntries').remove(entry);
-                    me.saveEntries();
+                listeners: {
+                    close: () => me.reloadPrefixEntries(),
                 },
             });
         },
@@ -441,9 +358,6 @@ Ext.define('PVE.sdn.PrefixListPanel', {
             border: false,
             split: true,
             reference: 'prefixListGrid',
-            bind: {
-                store: '{prefixLists}',
-            },
             listeners: {
                 select: 'selectPrefixList',
             },
@@ -454,7 +368,6 @@ Ext.define('PVE.sdn.PrefixListPanel', {
             border: false,
             bind: {
                 prefixList: '{prefixListGrid.selection}',
-                store: '{prefixListEntries}',
                 emptyText: '{entryGridEmptyText}',
             },
             reference: 'prefixListEntriesGrid',
-- 
2.47.3





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

* [PATCH pve-manager v6 20/24] ui: sdn: route maps: adapt to new route map api structure
  2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
                   ` (18 preceding siblings ...)
  2026-05-08 16:31 ` [PATCH pve-manager v6 19/24] ui: sdn: prefix list: adapt to changed api structure Stefan Hanreich
@ 2026-05-08 16:31 ` Stefan Hanreich
  2026-05-08 16:31 ` [PATCH pve-manager v6 21/24] ui: sdn: prefix lists: route maps: use integerfields for numbers Stefan Hanreich
                   ` (3 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2026-05-08 16:31 UTC (permalink / raw)
  To: pve-devel

Adapt the URLs here so they now use the new endpoints that have been
moved around. Additionally a new endpoint has been introduced
specifically to list route maps, so we do not need to transform the
output from the api in the route map selector anymore.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 www/manager6/sdn/RouteMapPanel.js    | 10 +++++-----
 www/manager6/sdn/RouteMapSelector.js | 17 -----------------
 2 files changed, 5 insertions(+), 22 deletions(-)

diff --git a/www/manager6/sdn/RouteMapPanel.js b/www/manager6/sdn/RouteMapPanel.js
index 5fa0475c3..8da030bb6 100644
--- a/www/manager6/sdn/RouteMapPanel.js
+++ b/www/manager6/sdn/RouteMapPanel.js
@@ -642,16 +642,16 @@ Ext.define('PVE.sdn.EditRouteMapEntryWindow', {
 
     loadUrl: function () {
         let me = this;
-        return `/api2/extjs/cluster/sdn/route-maps/${me.getRouteMapId()}/${me.getOrder()}`;
+        return `/api2/extjs/cluster/sdn/route-maps/entries/${me.getRouteMapId()}/entry/${me.getOrder()}`;
     },
 
     submitUrl: function () {
         let me = this;
 
         if (me.isCreate) {
-            return '/api2/extjs/cluster/sdn/route-maps';
+            return '/api2/extjs/cluster/sdn/route-maps/entries';
         } else {
-            return `/api2/extjs/cluster/sdn/route-maps/${me.getRouteMapId()}/${me.getOrder()}`;
+            return `/api2/extjs/cluster/sdn/route-maps/entries/${me.getRouteMapId()}/entry/${me.getOrder()}`;
         }
     },
 
@@ -752,7 +752,7 @@ Ext.define('PVE.sdn.RouteMapPanel', {
         model: 'PVE.sdn.RouteMapEntry',
         proxy: {
             type: 'proxmox',
-            url: '/api2/extjs/cluster/sdn/route-maps?pending=1',
+            url: '/api2/extjs/cluster/sdn/route-maps/entries?pending=1&verbose=1',
         },
         sorters: [
             {
@@ -821,7 +821,7 @@ Ext.define('PVE.sdn.RouteMapPanel', {
                     }
 
                     Proxmox.Async.api2({
-                        url: `/api2/extjs/cluster/sdn/route-maps/${entry.getRouteMapId()}/${entry.getOrder()}`,
+                        url: `/api2/extjs/cluster/sdn/route-maps/entries/${entry.getRouteMapId()}/entry/${entry.getOrder()}`,
                         method: 'DELETE',
                     })
                         .catch(Proxmox.Utils.alertResponseFailure)
diff --git a/www/manager6/sdn/RouteMapSelector.js b/www/manager6/sdn/RouteMapSelector.js
index 3a2770798..94d278137 100644
--- a/www/manager6/sdn/RouteMapSelector.js
+++ b/www/manager6/sdn/RouteMapSelector.js
@@ -24,23 +24,6 @@ Ext.define('PVE.sdn.RouteMapSelector', {
         proxy: {
             type: 'proxmox',
             url: '/api2/json/cluster/sdn/route-maps',
-            reader: {
-                transform: {
-                    fn: function (response) {
-                        return Object.values(
-                            response.data.reduce((accumulator, routeMapEntry) => {
-                                let id = routeMapEntry['route-map-id'];
-
-                                accumulator[id] ??= {
-                                    id,
-                                };
-
-                                return accumulator;
-                            }, {}),
-                        );
-                    },
-                },
-            },
         },
     },
     listConfig: {
-- 
2.47.3





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

* [PATCH pve-manager v6 21/24] ui: sdn: prefix lists: route maps: use integerfields for numbers
  2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
                   ` (19 preceding siblings ...)
  2026-05-08 16:31 ` [PATCH pve-manager v6 20/24] ui: sdn: route maps: adapt to new route map " Stefan Hanreich
@ 2026-05-08 16:31 ` Stefan Hanreich
  2026-05-08 16:31 ` [PATCH pve-manager v6 22/24] ui: sdn: prefix list panel: reload data on deleting prefix list entry Stefan Hanreich
                   ` (2 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2026-05-08 16:31 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 www/manager6/sdn/PrefixListPanel.js | 4 ++--
 www/manager6/sdn/RouteMapPanel.js   | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/www/manager6/sdn/PrefixListPanel.js b/www/manager6/sdn/PrefixListPanel.js
index 0e5cc11b8..793c30540 100644
--- a/www/manager6/sdn/PrefixListPanel.js
+++ b/www/manager6/sdn/PrefixListPanel.js
@@ -79,12 +79,12 @@ Ext.define('PVE.sdn.EditPrefixListEntryWindow', {
             vtype: 'IP64CIDRAddress',
         },
         {
-            xtype: 'proxmoxtextfield',
+            xtype: 'proxmoxintegerfield',
             fieldLabel: gettext('Prefix <='),
             name: 'le',
         },
         {
-            xtype: 'proxmoxtextfield',
+            xtype: 'proxmoxintegerfield',
             fieldLabel: gettext('Prefix >='),
             name: 'ge',
         },
diff --git a/www/manager6/sdn/RouteMapPanel.js b/www/manager6/sdn/RouteMapPanel.js
index 8da030bb6..f7f8571fb 100644
--- a/www/manager6/sdn/RouteMapPanel.js
+++ b/www/manager6/sdn/RouteMapPanel.js
@@ -689,7 +689,7 @@ Ext.define('PVE.sdn.EditRouteMapEntryWindow', {
             },
         },
         {
-            xtype: 'proxmoxtextfield',
+            xtype: 'proxmoxintegerfield',
             name: 'order',
             fieldLabel: gettext('Order'),
             bind: {
-- 
2.47.3





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

* [PATCH pve-manager v6 22/24] ui: sdn: prefix list panel: reload data on deleting prefix list entry
  2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
                   ` (20 preceding siblings ...)
  2026-05-08 16:31 ` [PATCH pve-manager v6 21/24] ui: sdn: prefix lists: route maps: use integerfields for numbers Stefan Hanreich
@ 2026-05-08 16:31 ` Stefan Hanreich
  2026-05-08 16:31 ` [PATCH pve-manager v6 23/24] ui: prefix list panel: delete empty le and get properties Stefan Hanreich
  2026-05-08 16:31 ` [PATCH pve-manager v6 24/24] ui: prefix list entry panel: make prefix required Stefan Hanreich
  23 siblings, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2026-05-08 16:31 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 www/manager6/sdn/PrefixListPanel.js | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/www/manager6/sdn/PrefixListPanel.js b/www/manager6/sdn/PrefixListPanel.js
index 793c30540..85d3141a6 100644
--- a/www/manager6/sdn/PrefixListPanel.js
+++ b/www/manager6/sdn/PrefixListPanel.js
@@ -250,6 +250,9 @@ Ext.define('PVE.sdn.PrefixListEntriesView', {
                 let seq = rec.data.seq;
                 return `/cluster/sdn/prefix-lists/${id}/entries/${seq}`;
             },
+            callback: function() {
+                this.up('grid').getStore().load();
+            }
         },
     ],
 
-- 
2.47.3





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

* [PATCH pve-manager v6 23/24] ui: prefix list panel: delete empty le and get properties
  2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
                   ` (21 preceding siblings ...)
  2026-05-08 16:31 ` [PATCH pve-manager v6 22/24] ui: sdn: prefix list panel: reload data on deleting prefix list entry Stefan Hanreich
@ 2026-05-08 16:31 ` Stefan Hanreich
  2026-05-08 16:31 ` [PATCH pve-manager v6 24/24] ui: prefix list entry panel: make prefix required Stefan Hanreich
  23 siblings, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2026-05-08 16:31 UTC (permalink / raw)
  To: pve-devel; +Cc: Lukas Sichert

Reported-by: Lukas Sichert <l.sichert@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 www/manager6/sdn/PrefixListPanel.js | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/www/manager6/sdn/PrefixListPanel.js b/www/manager6/sdn/PrefixListPanel.js
index 85d3141a6..18fc4aa41 100644
--- a/www/manager6/sdn/PrefixListPanel.js
+++ b/www/manager6/sdn/PrefixListPanel.js
@@ -82,11 +82,17 @@ Ext.define('PVE.sdn.EditPrefixListEntryWindow', {
             xtype: 'proxmoxintegerfield',
             fieldLabel: gettext('Prefix <='),
             name: 'le',
+            cbind: {
+                deleteEmpty: '{!isCreate}',
+            }
         },
         {
             xtype: 'proxmoxintegerfield',
             fieldLabel: gettext('Prefix >='),
             name: 'ge',
+            cbind: {
+                deleteEmpty: '{!isCreate}',
+            }
         },
     ],
 
-- 
2.47.3





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

* [PATCH pve-manager v6 24/24] ui: prefix list entry panel: make prefix required
  2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
                   ` (22 preceding siblings ...)
  2026-05-08 16:31 ` [PATCH pve-manager v6 23/24] ui: prefix list panel: delete empty le and get properties Stefan Hanreich
@ 2026-05-08 16:31 ` Stefan Hanreich
  23 siblings, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2026-05-08 16:31 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 www/manager6/sdn/PrefixListPanel.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/www/manager6/sdn/PrefixListPanel.js b/www/manager6/sdn/PrefixListPanel.js
index 18fc4aa41..c6d812061 100644
--- a/www/manager6/sdn/PrefixListPanel.js
+++ b/www/manager6/sdn/PrefixListPanel.js
@@ -77,6 +77,7 @@ Ext.define('PVE.sdn.EditPrefixListEntryWindow', {
             fieldLabel: gettext('Prefix'),
             name: 'prefix',
             vtype: 'IP64CIDRAddress',
+            allowBlank: false,
         },
         {
             xtype: 'proxmoxintegerfield',
-- 
2.47.3





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

end of thread, other threads:[~2026-05-08 16:41 UTC | newest]

Thread overview: 25+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
2026-05-08 16:31 ` [PATCH proxmox-ve-rs v6 01/24] sdn: prefix lists: refactor section config and api format Stefan Hanreich
2026-05-08 16:31 ` [PATCH proxmox-ve-rs v6 02/24] prefix lists: implement validation for prefix lists Stefan Hanreich
2026-05-08 16:31 ` [PATCH proxmox-perl-rs v6 03/24] sdn: prefix lists: refactor existing API endpoint Stefan Hanreich
2026-05-08 16:31 ` [PATCH proxmox-perl-rs v6 04/24] sdn: prefix lists: add crud methods for prefix list entries Stefan Hanreich
2026-05-08 16:31 ` [PATCH proxmox-perl-rs v6 05/24] sdn: prefix lists: validate prefix lists Stefan Hanreich
2026-05-08 16:31 ` [PATCH proxmox-perl-rs v6 06/24] sdn: route maps: add route map list method Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-network v6 07/24] api: refactor route map api structure Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-network v6 08/24] api: refactor prefix list " Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 09/24] ui: sdn: add route map selector Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 10/24] ui: sdn: add prefix list selector Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 11/24] ui: sdn: add panel for managing prefix lists Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 12/24] ui: sdn: add panel for managing route map entries Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 13/24] ui: sdn: bgp controller: allow configuring route maps Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 14/24] ui: sdn: evpn " Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 15/24] ui: sdn: openfabric: add route filter Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 16/24] ui: sdn: ospf: add route filter setting Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 17/24] ui: sdn: prefix list: add missing subjects Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 18/24] sdn: do not fail rendering record data if pending property is missing Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 19/24] ui: sdn: prefix list: adapt to changed api structure Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 20/24] ui: sdn: route maps: adapt to new route map " Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 21/24] ui: sdn: prefix lists: route maps: use integerfields for numbers Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 22/24] ui: sdn: prefix list panel: reload data on deleting prefix list entry Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 23/24] ui: prefix list panel: delete empty le and get properties Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 24/24] ui: prefix list entry panel: make prefix required Stefan Hanreich

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