From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id B39B21FF13F for ; Thu, 26 Mar 2026 11:32:47 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 4949ACCF6; Thu, 26 Mar 2026 11:33:09 +0100 (CET) Date: Thu, 26 Mar 2026 11:32:33 +0100 From: Wolfgang Bumiller To: Stefan Hanreich Subject: Re: [PATCH proxmox-perl-rs 1/3] pve-rs: sdn: add route maps module Message-ID: References: <20260325094142.174364-1-s.hanreich@proxmox.com> <20260325094142.174364-13-s.hanreich@proxmox.com> MIME-Version: 1.0 Content-Type: text/plain; charset=us-ascii Content-Disposition: inline In-Reply-To: <20260325094142.174364-13-s.hanreich@proxmox.com> X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1774521105461 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.915 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_MAILER 2 Automated Mailer Tag Left in Email SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: MRLE5GLRPIXVAC3NMMCFDTLGMTLBPYBT X-Message-ID-Hash: MRLE5GLRPIXVAC3NMMCFDTLGMTLBPYBT X-MailFrom: w.bumiller@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: pve-devel@lists.proxmox.com X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: On Wed, Mar 25, 2026 at 10:41:23AM +0100, Stefan Hanreich wrote: > Exposes the functionality from ve-config to Perl by providing helpers > for instantiating the Rust configuration from Perl. The module also > contains the implementation for the CRUD API methods, which will be > used in the API methods in pve-network. > > Signed-off-by: Stefan Hanreich > --- > pve-rs/Cargo.toml | 1 + > pve-rs/Makefile | 1 + > pve-rs/src/bindings/sdn/mod.rs | 3 +- > pve-rs/src/bindings/sdn/route_maps.rs | 243 ++++++++++++++++++++++++++ > 4 files changed, 247 insertions(+), 1 deletion(-) > create mode 100644 pve-rs/src/bindings/sdn/route_maps.rs > > diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml > index 45389b5..42d19fe 100644 > --- a/pve-rs/Cargo.toml > +++ b/pve-rs/Cargo.toml > @@ -42,6 +42,7 @@ proxmox-notify = { version = "1", features = ["pve-context"] } > proxmox-oci = "0.2.1" > proxmox-openid = "1.0.2" > proxmox-resource-scheduling = "1.0.1" > +proxmox-schema = "5" > proxmox-section-config = "3" > proxmox-shared-cache = "1" > proxmox-subscription = "1" > diff --git a/pve-rs/Makefile b/pve-rs/Makefile > index 3bbc464..d662b00 100644 > --- a/pve-rs/Makefile > +++ b/pve-rs/Makefile > @@ -31,6 +31,7 @@ PERLMOD_PACKAGES := \ > PVE::RS::OpenId \ > PVE::RS::ResourceScheduling::Static \ > PVE::RS::SDN::Fabrics \ > + PVE::RS::SDN::RouteMaps \ > PVE::RS::SDN \ > PVE::RS::TFA > > diff --git a/pve-rs/src/bindings/sdn/mod.rs b/pve-rs/src/bindings/sdn/mod.rs > index fde3138..c571d28 100644 > --- a/pve-rs/src/bindings/sdn/mod.rs > +++ b/pve-rs/src/bindings/sdn/mod.rs > @@ -1,4 +1,5 @@ > pub(crate) mod fabrics; > +pub(crate) mod route_maps; > > #[perlmod::package(name = "PVE::RS::SDN", lib = "pve_rs")] > pub mod pve_rs_sdn { > @@ -7,7 +8,7 @@ pub mod pve_rs_sdn { > //! This provides general methods for generating the frr config. > > use anyhow::Error; > - use proxmox_frr::ser::{FrrConfig, serializer::to_raw_config}; > + use proxmox_frr::ser::{serializer::to_raw_config, FrrConfig}; > > use proxmox_ve_config::common::valid::Validatable; > use proxmox_ve_config::sdn::fabric::section_config::node::NodeId; > diff --git a/pve-rs/src/bindings/sdn/route_maps.rs b/pve-rs/src/bindings/sdn/route_maps.rs > new file mode 100644 > index 0000000..b80126a > --- /dev/null > +++ b/pve-rs/src/bindings/sdn/route_maps.rs > @@ -0,0 +1,243 @@ > +#[perlmod::package(name = "PVE::RS::SDN::RouteMaps", lib = "pve_rs")] > +pub mod pve_rs_sdn_route_maps { > + //! The `PVE::RS::SDN::RouteMaps` package. > + > + use std::collections::HashMap; > + use std::ops::Deref; > + use std::sync::Mutex; > + > + use anyhow::{anyhow, Error}; > + use openssl::hash::{hash, MessageDigest}; > + use serde::{Deserialize, Serialize}; > + > + use perlmod::Value; > + > + use proxmox_schema::Updater; > + use proxmox_section_config::typed::{ApiSectionDataEntry, SectionConfigData}; > + use proxmox_ve_config::sdn::route_map::api::RouteMapDeletableProperties; > + use proxmox_ve_config::sdn::route_map::api::RouteMapEntry as ApiRouteMap; > + use proxmox_ve_config::sdn::route_map::api::RouteMapEntryUpdater; > + use proxmox_ve_config::sdn::route_map::RouteMap as ConfigRouteMap; > + use proxmox_ve_config::sdn::route_map::RouteMapEntryId; > + use proxmox_ve_config::sdn::route_map::RouteMapId; > + > + /// A SDN RouteMap config instance. > + #[derive(Serialize, Deserialize)] > + pub struct PerlRouteMapConfig { > + /// The route map config instance > + pub route_maps: Mutex>, > + } > + > + perlmod::declare_magic!(Box : &PerlRouteMapConfig as "PVE::RS::SDN::RouteMaps::Config"); > + > + /// Class method: Parse the raw configuration from `/etc/pve/sdn/route-maps.cfg`. > + #[export] > + pub fn config(#[raw] class: Value, raw_config: &[u8]) -> Result { > + let raw_config = std::str::from_utf8(raw_config)?; > + let config = ConfigRouteMap::parse_section_config("route-maps.cfg", raw_config)?; > + > + Ok( > + perlmod::instantiate_magic!(&class, MAGIC => Box::new(PerlRouteMapConfig { > + route_maps: Mutex::new(config.deref().clone()), > + })), > + ) > + } > + > + /// Class method: Parse the configuration from `/etc/pve/sdn/.running_config`. > + #[export] > + pub fn running_config( > + #[raw] class: Value, > + route_maps: HashMap, > + ) -> Result { > + Ok( > + perlmod::instantiate_magic!(&class, MAGIC => Box::new(PerlRouteMapConfig { > + route_maps: Mutex::new(route_maps.clone()), > + })), > + ) > + } > + > + /// Used for writing the running configuration. > + #[export] > + pub fn to_sections( > + #[try_from_ref] this: &PerlRouteMapConfig, > + ) -> Result, Error> { > + let config = this.route_maps.lock().unwrap(); > + Ok(config.deref().clone()) > + } > + > + /// Method: Convert the configuration into the section config string. > + /// > + /// Used for writing `/etc/pve/sdn/route-maps.cfg` > + #[export] > + pub fn to_raw(#[try_from_ref] this: &PerlRouteMapConfig) -> Result { > + let config = this.route_maps.lock().unwrap(); > + let route_maps: SectionConfigData = > + SectionConfigData::from_iter(config.deref().clone()); > + > + ConfigRouteMap::write_section_config("route-maps.cfg", &route_maps) > + } > + > + /// Method: Generate a digest for the whole configuration. > + #[export] > + pub fn digest(#[try_from_ref] this: &PerlRouteMapConfig) -> Result { > + let config = to_raw(this)?; > + let hash = hash(MessageDigest::sha256(), config.as_bytes())?; > + > + Ok(hex::encode(hash)) > + } > + > + /// Returns a list of all RouteMap entries. ^ Method: > + #[export] > + pub fn list( > + #[try_from_ref] this: &PerlRouteMapConfig, > + ) -> Result, Error> { > + Ok(this > + .route_maps > + .lock() > + .unwrap() > + .iter() > + .map(|(id, route_map_entry)| { > + let ConfigRouteMap::RouteMapEntry(route_map) = route_map_entry; > + (id.clone(), route_map.clone().into()) > + }) > + .collect()) > + } > + > + /// Returns a list of all RouteMap entries for a given RouteMap ID. ^ Method: and it doesn't return a list - which in perl is quite specific. (Note that perlmod gained support for returning a *list* via `perlmod::ser::Return`'s `List(T)` variant, as well as a `@`-like final list parameter via the `#[list]` attribute. Also note that the latter is not meant to be used for anything other than implementing a pre-existing *perl* API) > + #[export] > + pub fn list_route_map( > + #[try_from_ref] this: &PerlRouteMapConfig, > + route_map_id: RouteMapId, > + ) -> Result, Error> { > + Ok(this > + .route_maps > + .lock() > + .unwrap() > + .iter() > + .filter_map(|(id, route_map_entry)| { > + let ConfigRouteMap::RouteMapEntry(route_map) = route_map_entry; > + > + if route_map.id().route_map_id() == &route_map_id { > + return Some((id.clone(), route_map.clone().into())); > + } > + > + None > + }) > + .collect()) > + } > + > + /// Create a new RouteMap entry. ^ more missing `Method:` annotations follow... > + #[export] > + pub fn create( > + #[try_from_ref] this: &PerlRouteMapConfig, > + route_map: ApiRouteMap, > + ) -> Result<(), Error> { > + let mut route_maps = this.route_maps.lock().unwrap(); > + > + let id = > + RouteMapEntryId::new(route_map.route_map_id().clone(), route_map.order()).to_string(); So the key we use in `route_maps` is constructed from the route_map's id and order... > + let config_route_map = ConfigRouteMap::RouteMapEntry(route_map.into()); > + > + if route_maps.get(&id).is_some() { > + anyhow::bail!("route map entry already exists in configuration: {}", id); > + } > + > + route_maps.insert(id, config_route_map); ^ The above two should probably use the entry api route_maps.entry() { Entry::Occupied(_) => bail!(...), Entry::Vacant(vacancy) => vacancy.insert(...), } > + > + Ok(()) > + } > + > + /// Returns a specfic entry of a RouteMap. > + #[export] > + pub fn get( > + #[try_from_ref] this: &PerlRouteMapConfig, > + route_map_id: RouteMapId, > + order: u32, > + ) -> Result, Error> { > + let id = RouteMapEntryId::new(route_map_id, order); > + > + Ok(this > + .route_maps > + .lock() > + .unwrap() > + .iter() > + .find(|(_id, route_map_entry)| { > + let ConfigRouteMap::RouteMapEntry(route_map) = route_map_entry; > + route_map.id() == &id ...so could we just `.get()` with a `RouteMapEntryId::new(route_map_id, order).to_string()` here? > + }) > + .map(|(_id, route_map_entry)| { > + let ConfigRouteMap::RouteMapEntry(route_map) = route_map_entry; > + route_map.clone().into() > + })) > + } > + > + /// Update a RouteMap entry. > + #[export] > + pub fn update( > + #[try_from_ref] this: &PerlRouteMapConfig, > + route_map_id: RouteMapId, > + order: u32, > + updater: RouteMapEntryUpdater, > + delete: Option>, > + ) -> Result<(), Error> { > + if updater.is_empty() && delete.is_empty() { > + return Ok(()); > + } > + > + let mut route_maps = this.route_maps.lock().unwrap(); > + let id = RouteMapEntryId::new(route_map_id, order).to_string(); > + > + let ConfigRouteMap::RouteMapEntry(route_map) = route_maps > + .get_mut(&id) > + .ok_or_else(|| anyhow!("Could not find route map with id: {}", id))?; > + > + let RouteMapEntryUpdater { > + action, > + set_actions, > + match_actions, > + } = updater; > + > + if let Some(action) = action { > + route_map.set_action(action); > + } > + > + if let Some(match_actions) = match_actions { > + route_map.set_match_actions(match_actions); > + } > + > + if let Some(set_actions) = set_actions { > + route_map.set_set_actions(set_actions); > + } > + > + for deletable_property in delete.unwrap_or_default() { > + match deletable_property { > + RouteMapDeletableProperties::SetActions => { > + route_map.set_set_actions(Vec::new()); > + } > + RouteMapDeletableProperties::MatchActions => { > + route_map.set_match_actions(Vec::new()); > + } > + } > + } > + > + Ok(()) > + } > + > + /// Delete an entry in a RouteMap. > + #[export] > + pub fn delete( > + #[try_from_ref] this: &PerlRouteMapConfig, > + route_map_id: RouteMapId, > + order: u32, > + ) -> Result<(), Error> { > + let id = RouteMapEntryId::new(route_map_id, order).to_string(); > + > + this.route_maps > + .lock() > + .unwrap() > + .remove(&id.to_string()) > + .ok_or_else(|| anyhow!("could not find route map entry with id: {id}"))?; > + > + Ok(()) > + } > +} > -- > 2.47.3