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 11/24] sdn: add ipam module
Date: Tue, 12 Nov 2024 13:25:49 +0100	[thread overview]
Message-ID: <20241112122602.88598-12-s.hanreich@proxmox.com> (raw)
In-Reply-To: <20241112122602.88598-1-s.hanreich@proxmox.com>

This module includes structs for representing the JSON schema from the
PVE ipam. Those can be used to parse the current IPAM state.

We also include a general Ipam struct, and provide a method for
converting the PVE IPAM to the general struct. The idea behind this
is that we have multiple IPAM plugins in PVE and will likely add
support for importing them in the future. With the split, we can have
our dedicated structs for representing the different data formats from
the different IPAM plugins and then convert them into a common
representation that can then be used internally, decoupling the
concrete plugin from the code using the IPAM configuration.

Enforcing the invariants the way we currently do adds a bit of 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 ipam 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>
---
 .../src/firewall/types/address.rs             |   8 +
 proxmox-ve-config/src/guest/vm.rs             |   4 +
 proxmox-ve-config/src/sdn/ipam.rs             | 330 ++++++++++++++++++
 proxmox-ve-config/src/sdn/mod.rs              |   2 +
 4 files changed, 344 insertions(+)
 create mode 100644 proxmox-ve-config/src/sdn/ipam.rs

diff --git a/proxmox-ve-config/src/firewall/types/address.rs b/proxmox-ve-config/src/firewall/types/address.rs
index 57efb13..3777dc3 100644
--- a/proxmox-ve-config/src/firewall/types/address.rs
+++ b/proxmox-ve-config/src/firewall/types/address.rs
@@ -61,6 +61,14 @@ impl Cidr {
     pub fn is_ipv6(&self) -> bool {
         matches!(self, Cidr::Ipv6(_))
     }
+
+    pub fn contains_address(&self, ip: &IpAddr) -> bool {
+        match (self, ip) {
+            (Cidr::Ipv4(cidr), IpAddr::V4(ip)) => cidr.contains_address(ip),
+            (Cidr::Ipv6(cidr), IpAddr::V6(ip)) => cidr.contains_address(ip),
+            _ => false,
+        }
+    }
 }
 
 impl fmt::Display for Cidr {
diff --git a/proxmox-ve-config/src/guest/vm.rs b/proxmox-ve-config/src/guest/vm.rs
index ed6c66a..3476b93 100644
--- a/proxmox-ve-config/src/guest/vm.rs
+++ b/proxmox-ve-config/src/guest/vm.rs
@@ -18,6 +18,10 @@ static LOCAL_PART: [u8; 8] = [0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
 static EUI64_MIDDLE_PART: [u8; 2] = [0xFF, 0xFE];
 
 impl MacAddress {
+    pub fn new(address: [u8; 6]) -> Self {
+        Self(address)
+    }
+
     /// generates a link local IPv6-address according to RFC 4291 (Appendix A)
     pub fn eui64_link_local_address(&self) -> Ipv6Addr {
         let head = &self.0[..3];
diff --git a/proxmox-ve-config/src/sdn/ipam.rs b/proxmox-ve-config/src/sdn/ipam.rs
new file mode 100644
index 0000000..95e3ba7
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/ipam.rs
@@ -0,0 +1,330 @@
+use std::{
+    collections::{BTreeMap, HashMap},
+    error::Error,
+    fmt::Display,
+    net::IpAddr,
+};
+
+use serde::Deserialize;
+
+use crate::{
+    firewall::types::Cidr,
+    guest::{types::Vmid, vm::MacAddress},
+    sdn::{SdnNameError, SubnetName, ZoneName},
+};
+
+/// Struct for deserializing a gateway entry in PVE IPAM.
+///
+/// They are automatically generated by the PVE SDN module when creating a new subnet.
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct IpamJsonDataGateway {
+    #[serde(rename = "gateway")]
+    _gateway: u8,
+}
+
+/// Struct for deserializing a guest entry in PVE IPAM.
+///
+/// They are automatically created when adding a guest to a VNet that has a Subnet with DHCP
+/// configured.
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct IpamJsonDataVm {
+    vmid: Vmid,
+    hostname: Option<String>,
+    mac: MacAddress,
+}
+
+/// Struct for deserializing a custom entry in PVE IPAM.
+///
+/// Custom entries are created manually by the user via the Web UI / API.
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct IpamJsonDataCustom {
+    mac: MacAddress,
+}
+
+/// Enum representing the different kinds of entries that can be located in PVE IPAM.
+///
+/// For more information about the members see the documentation of the respective structs in the
+/// enum.
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+#[serde(untagged)]
+pub enum IpamJsonData {
+    Vm(IpamJsonDataVm),
+    Gateway(IpamJsonDataGateway),
+    Custom(IpamJsonDataCustom),
+}
+
+/// Struct for deserializing IPs from the PVE IPAM.
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
+pub struct IpJson {
+    ips: BTreeMap<IpAddr, IpamJsonData>,
+}
+
+/// Struct for deserializing subnets from the PVE IPAM.
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
+pub struct SubnetJson {
+    subnets: BTreeMap<Cidr, IpJson>,
+}
+
+/// Struct for deserializing the PVE IPAM.
+///
+/// It is usually located in `/etc/pve/priv/ipam.db`
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
+pub struct IpamJson {
+    zones: BTreeMap<ZoneName, SubnetJson>,
+}
+
+/// Holds the data for the IPAM entry of a VM.
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct IpamDataVm {
+    ip: IpAddr,
+    vmid: Vmid,
+    mac: MacAddress,
+    hostname: Option<String>,
+}
+
+impl IpamDataVm {
+    pub fn new(
+        ip: impl Into<IpAddr>,
+        vmid: impl Into<Vmid>,
+        mac: MacAddress,
+        hostname: impl Into<Option<String>>,
+    ) -> Self {
+        Self {
+            ip: ip.into(),
+            vmid: vmid.into(),
+            mac,
+            hostname: hostname.into(),
+        }
+    }
+
+    pub fn from_json_data(ip: IpAddr, data: IpamJsonDataVm) -> Self {
+        Self::new(ip, data.vmid, data.mac, data.hostname)
+    }
+
+    pub fn ip(&self) -> &IpAddr {
+        &self.ip
+    }
+
+    pub fn vmid(&self) -> Vmid {
+        self.vmid
+    }
+
+    pub fn mac(&self) -> &MacAddress {
+        &self.mac
+    }
+
+    pub fn hostname(&self) -> Option<&str> {
+        self.hostname.as_deref()
+    }
+}
+
+/// Holds the data for the IPAM entry of a Gateway.
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct IpamDataGateway {
+    ip: IpAddr,
+}
+
+impl IpamDataGateway {
+    pub fn new(ip: IpAddr) -> Self {
+        Self { ip }
+    }
+
+    fn from_json_data(ip: IpAddr, _json_data: IpamJsonDataGateway) -> Self {
+        Self::new(ip)
+    }
+
+    pub fn ip(&self) -> &IpAddr {
+        &self.ip
+    }
+}
+
+/// Holds the data for a custom IPAM entry.
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct IpamDataCustom {
+    ip: IpAddr,
+    mac: MacAddress,
+}
+
+impl IpamDataCustom {
+    pub fn new(ip: IpAddr, mac: MacAddress) -> Self {
+        Self { ip, mac }
+    }
+
+    fn from_json_data(ip: IpAddr, json_data: IpamJsonDataCustom) -> Self {
+        Self::new(ip, json_data.mac)
+    }
+
+    pub fn ip(&self) -> &IpAddr {
+        &self.ip
+    }
+
+    pub fn mac(&self) -> &MacAddress {
+        &self.mac
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub enum IpamData {
+    Vm(IpamDataVm),
+    Gateway(IpamDataGateway),
+    Custom(IpamDataCustom),
+}
+
+impl IpamData {
+    pub fn from_json_data(ip: IpAddr, json_data: IpamJsonData) -> Self {
+        match json_data {
+            IpamJsonData::Vm(json_data) => IpamDataVm::from_json_data(ip, json_data).into(),
+            IpamJsonData::Gateway(json_data) => {
+                IpamDataGateway::from_json_data(ip, json_data).into()
+            }
+            IpamJsonData::Custom(json_data) => IpamDataCustom::from_json_data(ip, json_data).into(),
+        }
+    }
+
+    pub fn ip_address(&self) -> &IpAddr {
+        match &self {
+            IpamData::Vm(data) => data.ip(),
+            IpamData::Gateway(data) => data.ip(),
+            IpamData::Custom(data) => data.ip(),
+        }
+    }
+}
+
+impl From<IpamDataVm> for IpamData {
+    fn from(value: IpamDataVm) -> Self {
+        IpamData::Vm(value)
+    }
+}
+
+impl From<IpamDataGateway> for IpamData {
+    fn from(value: IpamDataGateway) -> Self {
+        IpamData::Gateway(value)
+    }
+}
+
+impl From<IpamDataCustom> for IpamData {
+    fn from(value: IpamDataCustom) -> Self {
+        IpamData::Custom(value)
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub enum IpamError {
+    NameError(SdnNameError),
+    InvalidIpAddress,
+    DuplicateIpAddress,
+    IpAddressOutOfBounds,
+}
+
+impl Error for IpamError {}
+
+impl Display for IpamError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str("")
+    }
+}
+
+/// represents an entry in the PVE IPAM database
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct IpamEntry {
+    subnet: SubnetName,
+    data: IpamData,
+}
+
+impl IpamEntry {
+    /// Creates a new PVE IPAM entry.
+    ///
+    /// # Errors
+    ///
+    /// This function will return an error if the IP address of the entry does not match the CIDR
+    /// of the subnet.
+    pub fn new(subnet: SubnetName, data: IpamData) -> Result<Self, IpamError> {
+        if !subnet.cidr().contains_address(data.ip_address()) {
+            return Err(IpamError::IpAddressOutOfBounds);
+        }
+
+        Ok(IpamEntry { subnet, data })
+    }
+
+    pub fn subnet(&self) -> &SubnetName {
+        &self.subnet
+    }
+
+    pub fn data(&self) -> &IpamData {
+        &self.data
+    }
+
+    pub fn ip_address(&self) -> &IpAddr {
+        self.data.ip_address()
+    }
+}
+
+/// Common representation of IPAM data used in SDN.
+///
+/// This should be instantiated by reading from one of the concrete IPAM implementations and then
+/// converting into this common struct.
+///
+/// # Invariants
+/// * No IP address in a Subnet is allocated twice
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
+pub struct Ipam {
+    entries: BTreeMap<SubnetName, Vec<IpamEntry>>,
+}
+
+impl Ipam {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    pub fn from_entries(entries: impl IntoIterator<Item = IpamEntry>) -> Result<Self, IpamError> {
+        let mut ipam = Self::new();
+
+        for entry in entries {
+            ipam.add_entry(entry)?;
+        }
+
+        Ok(ipam)
+    }
+
+    /// Adds a new [`IpamEntry`] to the database.
+    ///
+    /// # Errors
+    ///
+    /// This function will return an error if the IP is already allocated by another guest.
+    pub fn add_entry(&mut self, entry: IpamEntry) -> Result<(), IpamError> {
+        if let Some(entries) = self.entries.get_mut(entry.subnet()) {
+            for ipam_entry in &*entries {
+                if ipam_entry.ip_address() == entry.ip_address() {
+                    return Err(IpamError::DuplicateIpAddress);
+                }
+            }
+
+            entries.push(entry);
+        } else {
+            self.entries
+                .insert(entry.subnet().clone(), [entry].to_vec());
+        }
+
+        Ok(())
+    }
+}
+
+impl TryFrom<IpamJson> for Ipam {
+    type Error = IpamError;
+
+    fn try_from(value: IpamJson) -> Result<Self, Self::Error> {
+        let mut ipam = Ipam::default();
+
+        for (zone_name, subnet_json) in value.zones {
+            for (cidr, ip_json) in subnet_json.subnets {
+                for (ip, json_data) in ip_json.ips {
+                    let data = IpamData::from_json_data(ip, json_data);
+                    let subnet = SubnetName::new(zone_name.clone(), cidr);
+                    ipam.add_entry(IpamEntry::new(subnet, data)?)?;
+                }
+            }
+        }
+
+        Ok(ipam)
+    }
+}
diff --git a/proxmox-ve-config/src/sdn/mod.rs b/proxmox-ve-config/src/sdn/mod.rs
index 0752631..0ea874f 100644
--- a/proxmox-ve-config/src/sdn/mod.rs
+++ b/proxmox-ve-config/src/sdn/mod.rs
@@ -1,3 +1,5 @@
+pub mod ipam;
+
 use std::{error::Error, fmt::Display, str::FromStr};
 
 use serde_with::DeserializeFromStr;
-- 
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 ` Stefan Hanreich [this message]
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 ` [pve-devel] [PATCH proxmox-ve-rs v3 13/24] sdn: add config module Stefan Hanreich
2024-11-12 12:25 ` [pve-devel] [PATCH proxmox-ve-rs v3 14/24] sdn: config: add method for generating ipsets 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-12-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