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 398C81FF146 for ; Tue, 09 Jun 2026 15:26:46 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 290341267F; Tue, 9 Jun 2026 15:26:07 +0200 (CEST) From: Hannes Laimer To: pve-devel@lists.proxmox.com Subject: [PATCH proxmox-perl-rs 05/16] sdn: add microseg config binding Date: Tue, 9 Jun 2026 15:25:11 +0200 Message-ID: <20260609132522.235917-6-h.laimer@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260609132522.235917-1-h.laimer@proxmox.com> References: <20260609132522.235917-1-h.laimer@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1781011483593 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.916 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: DEYEHYDLJSSUQDBDCW2UZK4OMCVBTHHO X-Message-ID-Hash: DEYEHYDLJSSUQDBDCW2UZK4OMCVBTHHO X-MailFrom: h.laimer@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 X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Signed-off-by: Hannes Laimer --- pve-rs/Makefile | 1 + pve-rs/src/bindings/sdn/microseg.rs | 172 ++++++++++++++++++++++++++++ pve-rs/src/bindings/sdn/mod.rs | 1 + 3 files changed, 174 insertions(+) create mode 100644 pve-rs/src/bindings/sdn/microseg.rs diff --git a/pve-rs/Makefile b/pve-rs/Makefile index bb1cd2d..fc199ae 100644 --- a/pve-rs/Makefile +++ b/pve-rs/Makefile @@ -33,6 +33,7 @@ PERLMOD_PACKAGES := \ PVE::RS::ResourceScheduling::Static \ PVE::RS::ResourceScheduling::Dynamic \ PVE::RS::SDN::Fabrics \ + PVE::RS::SDN::Microseg \ PVE::RS::SDN::PrefixLists \ PVE::RS::SDN::RouteMaps \ PVE::RS::SDN::WireGuard::PrivateKeys \ diff --git a/pve-rs/src/bindings/sdn/microseg.rs b/pve-rs/src/bindings/sdn/microseg.rs new file mode 100644 index 0000000..fab11b0 --- /dev/null +++ b/pve-rs/src/bindings/sdn/microseg.rs @@ -0,0 +1,172 @@ +#[perlmod::package(name = "PVE::RS::SDN::Microseg", lib = "pve_rs")] +pub mod pve_rs_sdn_microseg { + //! The `PVE::RS::SDN::Microseg` package. + //! + //! This provides the configuration for SDN microseg, as well as helper methods for reading + //! / writing the configuration. + + use std::collections::{BTreeMap, HashMap}; + use std::ops::Deref; + use std::sync::Mutex; + + use anyhow::{Error, anyhow, bail}; + use openssl::hash::{MessageDigest, hash}; + + use perlmod::Value; + + use proxmox_section_config::typed::{ApiSectionDataEntry, SectionConfigData}; + use proxmox_ve_config::sdn::microseg::api::{MicrosegCreate, MicrosegUpdate}; + use proxmox_ve_config::sdn::microseg::{self, MicrosegEntry}; + + /// A SDN Microseg config instance. + pub struct PerlMicrosegConfig { + /// The microseg config instance + pub microseg: Mutex>, + } + + perlmod::declare_magic!(Box : &PerlMicrosegConfig as "PVE::RS::SDN::Microseg::Config"); + + /// Class method: Parse the raw configuration from `/etc/pve/sdn/microseg.cfg`. + #[export] + pub fn config(#[raw] class: Value, raw_config: &[u8]) -> Result { + let raw_config = std::str::from_utf8(raw_config)?; + let config = MicrosegEntry::parse_section_config("microseg.cfg", raw_config)?; + + microseg::validate(config.deref())?; + + Ok( + perlmod::instantiate_magic!(&class, MAGIC => Box::new(PerlMicrosegConfig { + microseg: Mutex::new(config.deref().clone()), + })), + ) + } + + /// Class method: Parse the configuration from `/etc/pve/sdn/.running-config`. + #[export] + pub fn running_config( + #[raw] class: Value, + entries: HashMap, + ) -> Result { + microseg::validate(&entries)?; + + Ok( + perlmod::instantiate_magic!(&class, MAGIC => Box::new(PerlMicrosegConfig { + microseg: Mutex::new(entries), + })), + ) + } + + /// Method: Used for writing the running configuration. + #[export] + pub fn to_sections( + #[try_from_ref] this: &PerlMicrosegConfig, + ) -> Result, Error> { + let config = this.microseg.lock().unwrap(); + microseg::validate(config.deref())?; + Ok(config.deref().clone()) + } + + /// Method: Convert the configuration into the section config string. + /// + /// Used for writing `/etc/pve/sdn/microseg.cfg` + #[export] + pub fn to_raw(#[try_from_ref] this: &PerlMicrosegConfig) -> Result { + let config = this.microseg.lock().unwrap(); + microseg::validate(config.deref())?; + // write sections in id order so the output, and the digest taken over it, are stable + // across calls; the config is stored in a HashMap whose iteration order is not + let ordered: BTreeMap = config.deref().clone().into_iter().collect(); + let data: SectionConfigData = SectionConfigData::from_iter(ordered); + + MicrosegEntry::write_section_config("microseg.cfg", &data) + } + + /// Method: Generate a digest for the whole configuration. + #[export] + pub fn digest(#[try_from_ref] this: &PerlMicrosegConfig) -> Result { + let raw = to_raw(this)?; + let digest = hash(MessageDigest::sha256(), raw.as_bytes())?; + + Ok(hex::encode(digest)) + } + + /// Method: Returns all microseg objects, keyed by id. + #[export] + pub fn list( + #[try_from_ref] this: &PerlMicrosegConfig, + ) -> Result, Error> { + Ok(this.microseg.lock().unwrap().deref().clone()) + } + + /// Method: Returns a single microseg object. + #[export] + pub fn get( + #[try_from_ref] this: &PerlMicrosegConfig, + id: &str, + ) -> Result, Error> { + Ok(this.microseg.lock().unwrap().get(id).cloned()) + } + + /// Method: Create a new microseg object. A group with no mark gets the lowest free one. + #[export] + pub fn create( + #[try_from_ref] this: &PerlMicrosegConfig, + create: MicrosegCreate, + ) -> Result<(), Error> { + let mut entries = this.microseg.lock().unwrap(); + + let entry = microseg::api::build_entry(create, &entries)?; + let id = entry.id().to_string(); + + if entries.contains_key(&id) { + bail!("microseg object '{id}' already exists"); + } + + entries.insert(id, entry); + microseg::validate(&entries)?; + + Ok(()) + } + + /// Method: Update an existing microseg object. + #[export] + pub fn update( + #[try_from_ref] this: &PerlMicrosegConfig, + id: &str, + update: MicrosegUpdate, + ) -> Result<(), Error> { + let mut entries = this.microseg.lock().unwrap(); + + let mut entry = entries + .get(id) + .cloned() + .ok_or_else(|| anyhow!("microseg object '{id}' does not exist"))?; + + microseg::api::apply_update(&mut entry, update)?; + entries.insert(id.to_string(), entry); + microseg::validate(&entries)?; + + Ok(()) + } + + /// Method: Delete a microseg object. A group still referenced by a rule or assignment cannot + /// be removed. + #[export] + pub fn delete(#[try_from_ref] this: &PerlMicrosegConfig, id: &str) -> Result<(), Error> { + let mut entries = this.microseg.lock().unwrap(); + + if !entries.contains_key(id) { + bail!("microseg object '{id}' does not exist"); + } + + if matches!(entries.get(id), Some(MicrosegEntry::Group(_))) { + if let Some(referrer) = microseg::api::group_referenced_by(&entries, id) { + bail!("group '{id}' is still referenced by '{referrer}'"); + } + } + + entries.remove(id); + + Ok(()) + } +} diff --git a/pve-rs/src/bindings/sdn/mod.rs b/pve-rs/src/bindings/sdn/mod.rs index dcae046..1d4c23f 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 microseg; pub(crate) mod prefix_lists; pub(crate) mod route_maps; pub(crate) mod wireguard; -- 2.47.3