public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Stefan Hanreich <s.hanreich@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH proxmox-ve-rs v3 13/24] sdn: add config module
Date: Tue, 12 Nov 2024 13:25:51 +0100	[thread overview]
Message-ID: <20241112122602.88598-14-s.hanreich@proxmox.com> (raw)
In-Reply-To: <20241112122602.88598-1-s.hanreich@proxmox.com>

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 <s.hanreich@proxmox.com>
---
 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<Self, Self::Err> {
+        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<Self, Self::Err> {
+        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<DhcpType>,
+}
+
+/// Struct for deserializing the zones of the SDN running config
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Default)]
+pub struct ZonesRunningConfig {
+    ids: HashMap<ZoneName, ZoneRunningConfig>,
+}
+
+/// 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<DhcpRange> for IpRange {
+    type Error = IpRangeError;
+
+    fn try_from(value: DhcpRange) -> Result<Self, Self::Error> {
+        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<IpAddr>,
+    snat: Option<u8>,
+    #[serde(rename = "dhcp-range")]
+    dhcp_range: Option<Vec<PropertyString<DhcpRange>>>,
+}
+
+/// Struct for deserializing the subnets of the SDN running config
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Default)]
+pub struct SubnetsRunningConfig {
+    ids: HashMap<SubnetName, SubnetRunningConfig>,
+}
+
+/// 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<VnetName, VnetRunningConfig>,
+}
+
+/// 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<ZonesRunningConfig>,
+    subnets: Option<SubnetsRunningConfig>,
+    vnets: Option<VnetsRunningConfig>,
+}
+
+/// 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<IpAddr>,
+    snat: bool,
+    dhcp_range: Vec<IpRange>,
+}
+
+impl SubnetConfig {
+    pub fn new(
+        name: SubnetName,
+        gateway: impl Into<Option<IpAddr>>,
+        snat: bool,
+        dhcp_range: impl IntoIterator<Item = IpRange>,
+    ) -> Result<Self, SdnConfigError> {
+        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<Self, SdnConfigError> {
+        let snat = running_config
+            .snat
+            .map(|snat| snat != 0)
+            .unwrap_or_else(|| false);
+
+        let dhcp_range: Vec<IpRange> = match running_config.dhcp_range {
+            Some(dhcp_range) => dhcp_range
+                .into_iter()
+                .map(PropertyString::into_inner)
+                .map(IpRange::try_from)
+                .collect::<Result<Vec<IpRange>, 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<Item = &IpRange> + '_ {
+        self.dhcp_range.iter()
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct VnetConfig {
+    name: VnetName,
+    subnets: BTreeMap<Cidr, SubnetConfig>,
+}
+
+impl VnetConfig {
+    pub fn new(name: VnetName) -> Self {
+        Self {
+            name,
+            subnets: BTreeMap::default(),
+        }
+    }
+
+    pub fn from_subnets(
+        name: VnetName,
+        subnets: impl IntoIterator<Item = SubnetConfig>,
+    ) -> Result<Self, SdnConfigError> {
+        let mut config = Self::new(name);
+        config.add_subnets(subnets)?;
+        Ok(config)
+    }
+
+    pub fn add_subnets(
+        &mut self,
+        subnets: impl IntoIterator<Item = SubnetConfig>,
+    ) -> Result<(), SdnConfigError> {
+        self.subnets
+            .extend(subnets.into_iter().map(|subnet| (*subnet.cidr(), subnet)));
+        Ok(())
+    }
+
+    pub fn add_subnet(
+        &mut self,
+        subnet: SubnetConfig,
+    ) -> Result<Option<SubnetConfig>, SdnConfigError> {
+        Ok(self.subnets.insert(*subnet.cidr(), subnet))
+    }
+
+    pub fn subnets(&self) -> impl Iterator<Item = &SubnetConfig> + '_ {
+        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<VnetName, VnetConfig>,
+}
+
+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<Item = VnetConfig>,
+    ) -> Result<Self, SdnConfigError> {
+        let mut config = Self::new(name, ty);
+        config.add_vnets(vnets)?;
+        Ok(config)
+    }
+
+    pub fn add_vnets(
+        &mut self,
+        vnets: impl IntoIterator<Item = VnetConfig>,
+    ) -> Result<(), SdnConfigError> {
+        self.vnets
+            .extend(vnets.into_iter().map(|vnet| (vnet.name.clone(), vnet)));
+
+        Ok(())
+    }
+
+    pub fn add_vnet(&mut self, vnet: VnetConfig) -> Result<Option<VnetConfig>, SdnConfigError> {
+        Ok(self.vnets.insert(vnet.name.clone(), vnet))
+    }
+
+    pub fn vnets(&self) -> impl Iterator<Item = &VnetConfig> + '_ {
+        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<ZoneName, ZoneConfig>,
+}
+
+impl SdnConfig {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    pub fn from_zones(zones: impl IntoIterator<Item = ZoneConfig>) -> Result<Self, SdnConfigError> {
+        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<Item = ZoneConfig>,
+    ) -> 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<Option<ZoneConfig>, 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<Option<VnetConfig>, 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<Option<SubnetConfig>, 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<Item = &ZoneConfig> + '_ {
+        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<Item = (&ZoneConfig, &VnetConfig)> + '_ {
+        self.zones()
+            .flat_map(|zone| zone.vnets().map(move |vnet| (zone, vnet)))
+    }
+}
+
+impl TryFrom<RunningConfig> for SdnConfig {
+    type Error = SdnConfigError;
+
+    fn try_from(mut value: RunningConfig) -> Result<Self, Self::Error> {
+        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


  parent reply	other threads:[~2024-11-12 12:29 UTC|newest]

Thread overview: 26+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2024-11-12 12:25 [pve-devel] [PATCH docs/firewall/manager/proxmox{-ve-rs, -firewall, -perl-rs} v3 00/24] autogenerate ipsets for sdn objects Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 01/24] debian: add files for packaging Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 02/24] firewall: add sdn scope for ipsets Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 03/24] firewall: add ip range types Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 04/24] firewall: address: use new iprange type for ip entries Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 05/24] ipset: add range variant to addresses Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 06/24] iprange: add methods for converting an ip range to cidrs Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 07/24] ipset: address: add helper methods Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 08/24] firewall: guest: derive traits according to rust api guidelines Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 09/24] common: add allowlist Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 10/24] sdn: add name types Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 11/24] sdn: add ipam module Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 12/24] sdn: ipam: add method for generating ipsets Stefan Hanreich
2024-11-12 12:25 ` Stefan Hanreich [this message]
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 14/24] sdn: config: " Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 15/24] tests: add sdn config tests Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 16/24] tests: add ipam tests Stefan Hanreich
2024-11-12 19:16   ` [pve-devel] partially-applied-series: " Thomas Lamprecht
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-firewall v3 17/24] add proxmox-ve-rs crate - move proxmox-ve-config there Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-firewall v3 18/24] config: tests: add support for loading sdn and ipam config Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-firewall v3 19/24] ipsets: autogenerate ipsets for vnets and ipam Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH pve-firewall v3 20/24] add support for loading sdn firewall configuration Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH pve-firewall v3 21/24] api: load sdn ipsets Stefan Hanreich
2024-11-12 12:26 ` [pve-devel] [PATCH proxmox-perl-rs v3 22/24] add PVE::RS::Firewall::SDN module Stefan Hanreich
2024-11-12 12:26 ` [pve-devel] [PATCH pve-manager v3 23/24] firewall: add sdn scope to IPRefSelector Stefan Hanreich
2024-11-12 12:26 ` [pve-devel] [PATCH pve-docs v3 24/24] sdn: add documentation for firewall integration Stefan Hanreich

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20241112122602.88598-14-s.hanreich@proxmox.com \
    --to=s.hanreich@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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