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 E10981FF163 for ; Thu, 10 Oct 2024 17:59:15 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id BA27F1E62D; Thu, 10 Oct 2024 17:57:29 +0200 (CEST) From: Stefan Hanreich To: pve-devel@lists.proxmox.com Date: Thu, 10 Oct 2024 17:56:25 +0200 Message-Id: <20241010155637.255451-14-s.hanreich@proxmox.com> X-Mailer: git-send-email 2.39.5 In-Reply-To: <20241010155637.255451-1-s.hanreich@proxmox.com> References: <20241010155637.255451-1-s.hanreich@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.260 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 v2 13/25] sdn: add ipam 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" 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 --- .../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 6978a8f..a7bb6ad 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..682bbe7 --- /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, + 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, +} + +/// struct for deserializing subnets from the PVE IPAM +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord, Default)] +pub struct SubnetJson { + subnets: BTreeMap, +} + +/// 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, +} + +/// 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, +} + +impl IpamDataVm { + pub fn new( + ip: impl Into, + vmid: impl Into, + mac: MacAddress, + hostname: impl Into>, + ) -> 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 for IpamData { + fn from(value: IpamDataVm) -> Self { + IpamData::Vm(value) + } +} + +impl From for IpamData { + fn from(value: IpamDataGateway) -> Self { + IpamData::Gateway(value) + } +} + +impl From 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 { + 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>, +} + +impl Ipam { + pub fn new() -> Self { + Self::default() + } + + pub fn from_entries(entries: impl IntoIterator) -> Result { + 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 for Ipam { + type Error = IpamError; + + fn try_from(value: IpamJson) -> Result { + 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 4e7c525..67af24e 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