From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 1A5C71FF168 for ; Tue, 12 Nov 2024 13:29:05 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id DA36B1FD41; Tue, 12 Nov 2024 13:26:52 +0100 (CET) From: Stefan Hanreich To: pve-devel@lists.proxmox.com Date: Tue, 12 Nov 2024 13:25:51 +0100 Message-Id: <20241112122602.88598-14-s.hanreich@proxmox.com> X-Mailer: git-send-email 2.39.5 In-Reply-To: <20241112122602.88598-1-s.hanreich@proxmox.com> References: <20241112122602.88598-1-s.hanreich@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.248 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_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods RDNS_NONE 0.793 Delivered to internal network by a host with no rDNS SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_NONE 0.001 SPF: sender does not publish an SPF Record Subject: [pve-devel] [PATCH proxmox-ve-rs v3 13/24] sdn: add config module X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox VE development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pve-devel-bounces@lists.proxmox.com Sender: "pve-devel" Similar to how the IPAM module works, we separate the internal representation from the concrete schema of the configuration file. We provide structs for parsing the running SDN configuration and a struct that is used internally for representing an SDN configuration, as well as a method for converting the running configuration to the internal representation. This is necessary because there are two possible sources for the SDN configuration: The running configuration as well as the SectionConfig that contains possible changes from the UI, that have not yet been applied. Simlarly to the IPAM, enforcing the invariants the way we currently do adds some runtime complexity when building the object, but we get the upside of never being able to construct an invalid struct. For the amount of entries the sdn config usually has, this should be fine. Should it turn out to be not performant enough we could always add a HashSet for looking up values and speeding up the validation. For now, I wanted to avoid the additional complexity. Signed-off-by: Stefan Hanreich --- proxmox-ve-config/src/sdn/config.rs | 570 ++++++++++++++++++++++++++++ proxmox-ve-config/src/sdn/mod.rs | 1 + 2 files changed, 571 insertions(+) create mode 100644 proxmox-ve-config/src/sdn/config.rs diff --git a/proxmox-ve-config/src/sdn/config.rs b/proxmox-ve-config/src/sdn/config.rs new file mode 100644 index 0000000..b71084b --- /dev/null +++ b/proxmox-ve-config/src/sdn/config.rs @@ -0,0 +1,570 @@ +use std::{ + collections::{BTreeMap, HashMap}, + error::Error, + fmt::Display, + net::IpAddr, + str::FromStr, +}; + +use proxmox_schema::{property_string::PropertyString, ApiType, ObjectSchema, StringSchema}; + +use serde::Deserialize; +use serde_with::{DeserializeFromStr, SerializeDisplay}; + +use crate::{ + common::Allowlist, + firewall::types::{ + address::{IpRange, IpRangeError}, + ipset::{IpsetEntry, IpsetName, IpsetScope}, + Cidr, Ipset, + }, + sdn::{SdnNameError, SubnetName, VnetName, ZoneName}, +}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum SdnConfigError { + InvalidZoneType, + InvalidDhcpType, + ZoneNotFound, + VnetNotFound, + MismatchedCidrGateway, + MismatchedSubnetZone, + NameError(SdnNameError), + InvalidDhcpRange(IpRangeError), + DuplicateVnetName, +} + +impl Error for SdnConfigError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + SdnConfigError::NameError(e) => Some(e), + SdnConfigError::InvalidDhcpRange(e) => Some(e), + _ => None, + } + } +} + +impl Display for SdnConfigError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SdnConfigError::NameError(err) => write!(f, "invalid name: {err}"), + SdnConfigError::InvalidDhcpRange(err) => write!(f, "invalid dhcp range: {err}"), + SdnConfigError::ZoneNotFound => write!(f, "zone not found"), + SdnConfigError::VnetNotFound => write!(f, "vnet not found"), + SdnConfigError::MismatchedCidrGateway => { + write!(f, "mismatched ip address family for gateway and CIDR") + } + SdnConfigError::InvalidZoneType => write!(f, "invalid zone type"), + SdnConfigError::InvalidDhcpType => write!(f, "invalid dhcp type"), + SdnConfigError::DuplicateVnetName => write!(f, "vnet name occurs in multiple zones"), + SdnConfigError::MismatchedSubnetZone => { + write!(f, "subnet zone does not match actual zone") + } + } + } +} + +#[derive( + Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, SerializeDisplay, DeserializeFromStr, +)] +pub enum ZoneType { + Simple, + Vlan, + Qinq, + Vxlan, + Evpn, +} + +impl FromStr for ZoneType { + type Err = SdnConfigError; + + fn from_str(s: &str) -> Result { + match s { + "simple" => Ok(ZoneType::Simple), + "vlan" => Ok(ZoneType::Vlan), + "qinq" => Ok(ZoneType::Qinq), + "vxlan" => Ok(ZoneType::Vxlan), + "evpn" => Ok(ZoneType::Evpn), + _ => Err(SdnConfigError::InvalidZoneType), + } + } +} + +impl Display for ZoneType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + ZoneType::Simple => "simple", + ZoneType::Vlan => "vlan", + ZoneType::Qinq => "qinq", + ZoneType::Vxlan => "vxlan", + ZoneType::Evpn => "evpn", + }) + } +} + +#[derive( + Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, SerializeDisplay, DeserializeFromStr, +)] +pub enum DhcpType { + Dnsmasq, +} + +impl FromStr for DhcpType { + type Err = SdnConfigError; + + fn from_str(s: &str) -> Result { + match s { + "dnsmasq" => Ok(DhcpType::Dnsmasq), + _ => Err(SdnConfigError::InvalidDhcpType), + } + } +} + +impl Display for DhcpType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + DhcpType::Dnsmasq => "dnsmasq", + }) + } +} + +/// Struct for deserializing a zone entry of the SDN running config +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct ZoneRunningConfig { + #[serde(rename = "type")] + ty: ZoneType, + dhcp: Option, +} + +/// Struct for deserializing the zones of the SDN running config +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Default)] +pub struct ZonesRunningConfig { + ids: HashMap, +} + +/// Represents the dhcp-range property string used in the SDN configuration +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct DhcpRange { + #[serde(rename = "start-address")] + start: IpAddr, + #[serde(rename = "end-address")] + end: IpAddr, +} + +impl ApiType for DhcpRange { + const API_SCHEMA: proxmox_schema::Schema = ObjectSchema::new( + "DHCP range", + &[ + ( + "end-address", + false, + &StringSchema::new("end address of DHCP range").schema(), + ), + ( + "start-address", + false, + &StringSchema::new("start address of DHCP range").schema(), + ), + ], + ) + .schema(); +} + +impl TryFrom for IpRange { + type Error = IpRangeError; + + fn try_from(value: DhcpRange) -> Result { + IpRange::new(value.start, value.end) + } +} + +/// Struct for deserializing a subnet entry of the SDN running config +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct SubnetRunningConfig { + vnet: VnetName, + gateway: Option, + snat: Option, + #[serde(rename = "dhcp-range")] + dhcp_range: Option>>, +} + +/// Struct for deserializing the subnets of the SDN running config +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Default)] +pub struct SubnetsRunningConfig { + ids: HashMap, +} + +/// Struct for deserializing a vnet entry of the SDN running config +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct VnetRunningConfig { + zone: ZoneName, +} + +/// struct for deserializing the vnets of the SDN running config +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Default)] +pub struct VnetsRunningConfig { + ids: HashMap, +} + +/// Struct for deserializing the SDN running config +/// +/// usually taken from the content of /etc/pve/sdn/.running-config +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Default)] +pub struct RunningConfig { + zones: Option, + subnets: Option, + vnets: Option, +} + +/// A struct containing the configuration for an SDN subnet +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct SubnetConfig { + name: SubnetName, + gateway: Option, + snat: bool, + dhcp_range: Vec, +} + +impl SubnetConfig { + pub fn new( + name: SubnetName, + gateway: impl Into>, + snat: bool, + dhcp_range: impl IntoIterator, + ) -> Result { + let gateway = gateway.into(); + + if let Some(gateway) = gateway { + if !(gateway.is_ipv4() && name.cidr().is_ipv4() + || gateway.is_ipv6() && name.cidr().is_ipv6()) + { + return Err(SdnConfigError::MismatchedCidrGateway); + } + } + + Ok(Self { + name, + gateway, + snat, + dhcp_range: dhcp_range.into_iter().collect(), + }) + } + + pub fn try_from_running_config( + name: SubnetName, + running_config: SubnetRunningConfig, + ) -> Result { + let snat = running_config + .snat + .map(|snat| snat != 0) + .unwrap_or_else(|| false); + + let dhcp_range: Vec = match running_config.dhcp_range { + Some(dhcp_range) => dhcp_range + .into_iter() + .map(PropertyString::into_inner) + .map(IpRange::try_from) + .collect::, IpRangeError>>() + .map_err(SdnConfigError::InvalidDhcpRange)?, + None => Vec::new(), + }; + + Self::new(name, running_config.gateway, snat, dhcp_range) + } + + pub fn name(&self) -> &SubnetName { + &self.name + } + + pub fn gateway(&self) -> Option<&IpAddr> { + self.gateway.as_ref() + } + + pub fn snat(&self) -> bool { + self.snat + } + + pub fn cidr(&self) -> &Cidr { + self.name.cidr() + } + + pub fn dhcp_ranges(&self) -> impl Iterator + '_ { + self.dhcp_range.iter() + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct VnetConfig { + name: VnetName, + subnets: BTreeMap, +} + +impl VnetConfig { + pub fn new(name: VnetName) -> Self { + Self { + name, + subnets: BTreeMap::default(), + } + } + + pub fn from_subnets( + name: VnetName, + subnets: impl IntoIterator, + ) -> Result { + let mut config = Self::new(name); + config.add_subnets(subnets)?; + Ok(config) + } + + pub fn add_subnets( + &mut self, + subnets: impl IntoIterator, + ) -> Result<(), SdnConfigError> { + self.subnets + .extend(subnets.into_iter().map(|subnet| (*subnet.cidr(), subnet))); + Ok(()) + } + + pub fn add_subnet( + &mut self, + subnet: SubnetConfig, + ) -> Result, SdnConfigError> { + Ok(self.subnets.insert(*subnet.cidr(), subnet)) + } + + pub fn subnets(&self) -> impl Iterator + '_ { + self.subnets.values() + } + + pub fn subnet(&self, cidr: &Cidr) -> Option<&SubnetConfig> { + self.subnets.get(cidr) + } + + pub fn name(&self) -> &VnetName { + &self.name + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct ZoneConfig { + name: ZoneName, + ty: ZoneType, + vnets: BTreeMap, +} + +impl ZoneConfig { + pub fn new(name: ZoneName, ty: ZoneType) -> Self { + Self { + name, + ty, + vnets: BTreeMap::default(), + } + } + + pub fn from_vnets( + name: ZoneName, + ty: ZoneType, + vnets: impl IntoIterator, + ) -> Result { + let mut config = Self::new(name, ty); + config.add_vnets(vnets)?; + Ok(config) + } + + pub fn add_vnets( + &mut self, + vnets: impl IntoIterator, + ) -> Result<(), SdnConfigError> { + self.vnets + .extend(vnets.into_iter().map(|vnet| (vnet.name.clone(), vnet))); + + Ok(()) + } + + pub fn add_vnet(&mut self, vnet: VnetConfig) -> Result, SdnConfigError> { + Ok(self.vnets.insert(vnet.name.clone(), vnet)) + } + + pub fn vnets(&self) -> impl Iterator + '_ { + self.vnets.values() + } + + pub fn vnet(&self, name: &VnetName) -> Option<&VnetConfig> { + self.vnets.get(name) + } + + pub fn vnet_mut(&mut self, name: &VnetName) -> Option<&mut VnetConfig> { + self.vnets.get_mut(name) + } + + pub fn name(&self) -> &ZoneName { + &self.name + } + + pub fn ty(&self) -> ZoneType { + self.ty + } +} + +/// Representation of a Proxmox VE SDN configuration +/// +/// This struct should not be instantiated directly but rather through reading the configuration +/// from a concrete config struct (e.g [`RunningConfig`]) and then converting into this common +/// representation. +/// +/// # Invariants +/// * Every Vnet name is unique, even if they are in different zones +/// * Subnets can only be added to a zone if their name contains the same zone they are added to +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord, Default)] +pub struct SdnConfig { + zones: BTreeMap, +} + +impl SdnConfig { + pub fn new() -> Self { + Self::default() + } + + pub fn from_zones(zones: impl IntoIterator) -> Result { + let mut config = Self::default(); + config.add_zones(zones)?; + Ok(config) + } + + /// adds a collection of zones to the configuration, overwriting existing zones with the same + /// name + pub fn add_zones( + &mut self, + zones: impl IntoIterator, + ) -> Result<(), SdnConfigError> { + for zone in zones { + self.add_zone(zone)?; + } + + Ok(()) + } + + /// adds a zone to the configuration, returning the old zone config if the zone already existed + pub fn add_zone(&mut self, mut zone: ZoneConfig) -> Result, SdnConfigError> { + let vnets = std::mem::take(&mut zone.vnets); + + let zone_name = zone.name().clone(); + let old_zone = self.zones.insert(zone_name.clone(), zone); + + for vnet in vnets.into_values() { + self.add_vnet(&zone_name, vnet)?; + } + + Ok(old_zone) + } + + pub fn add_vnet( + &mut self, + zone_name: &ZoneName, + mut vnet: VnetConfig, + ) -> Result, SdnConfigError> { + for zone in self.zones.values() { + if zone.name() != zone_name && zone.vnets.contains_key(vnet.name()) { + return Err(SdnConfigError::DuplicateVnetName); + } + } + + if let Some(zone) = self.zones.get_mut(zone_name) { + let subnets = std::mem::take(&mut vnet.subnets); + + let vnet_name = vnet.name().clone(); + let old_vnet = zone.vnets.insert(vnet_name.clone(), vnet); + + for subnet in subnets.into_values() { + self.add_subnet(zone_name, &vnet_name, subnet)?; + } + + return Ok(old_vnet); + } + + Err(SdnConfigError::ZoneNotFound) + } + + pub fn add_subnet( + &mut self, + zone_name: &ZoneName, + vnet_name: &VnetName, + subnet: SubnetConfig, + ) -> Result, SdnConfigError> { + if zone_name != subnet.name().zone() { + return Err(SdnConfigError::MismatchedSubnetZone); + } + + if let Some(zone) = self.zones.get_mut(zone_name) { + if let Some(vnet) = zone.vnets.get_mut(vnet_name) { + return Ok(vnet.subnets.insert(*subnet.name().cidr(), subnet)); + } else { + return Err(SdnConfigError::VnetNotFound); + } + } + + Err(SdnConfigError::ZoneNotFound) + } + + pub fn zone(&self, name: &ZoneName) -> Option<&ZoneConfig> { + self.zones.get(name) + } + + pub fn zones(&self) -> impl Iterator + '_ { + self.zones.values() + } + + pub fn vnet(&self, name: &VnetName) -> Option<(&ZoneConfig, &VnetConfig)> { + // we can do this because we enforce the invariant that every VNet name must be unique! + for zone in self.zones.values() { + if let Some(vnet) = zone.vnet(name) { + return Some((zone, vnet)); + } + } + + None + } + + pub fn vnets(&self) -> impl Iterator + '_ { + self.zones() + .flat_map(|zone| zone.vnets().map(move |vnet| (zone, vnet))) + } +} + +impl TryFrom for SdnConfig { + type Error = SdnConfigError; + + fn try_from(mut value: RunningConfig) -> Result { + let mut config = SdnConfig::default(); + + if let Some(running_zones) = value.zones.take() { + config.add_zones( + running_zones + .ids + .into_iter() + .map(|(name, running_config)| ZoneConfig::new(name, running_config.ty)), + )?; + } + + if let Some(running_vnets) = value.vnets.take() { + for (name, running_config) in running_vnets.ids { + config.add_vnet(&running_config.zone, VnetConfig::new(name))?; + } + } + + if let Some(running_subnets) = value.subnets.take() { + for (name, running_config) in running_subnets.ids { + let zone_name = name.zone().clone(); + let vnet_name = running_config.vnet.clone(); + + config.add_subnet( + &zone_name, + &vnet_name, + SubnetConfig::try_from_running_config(name, running_config)?, + )?; + } + } + + Ok(config) + } +} diff --git a/proxmox-ve-config/src/sdn/mod.rs b/proxmox-ve-config/src/sdn/mod.rs index 0ea874f..c8dc724 100644 --- a/proxmox-ve-config/src/sdn/mod.rs +++ b/proxmox-ve-config/src/sdn/mod.rs @@ -1,3 +1,4 @@ +pub mod config; pub mod ipam; use std::{error::Error, fmt::Display, str::FromStr}; -- 2.39.5 _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel