* [pbs-devel] [PATCH proxmox{-ve-rs, , -backup, -firewall, -network-interface-pinning} 0/8] proxmox-network-interface-pinning
@ 2025-07-29 16:56 Stefan Hanreich
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox-ve-rs 1/1] host: network: move to proxmox-network-api Stefan Hanreich
` (8 more replies)
0 siblings, 9 replies; 16+ messages in thread
From: Stefan Hanreich @ 2025-07-29 16:56 UTC (permalink / raw)
To: pbs-devel
Introduce the proxmox-network-interface-pinning tool for PBS, written in Rust.
I basically had to do the same changes to the network stack in PBS, that I
already had to do for Proxmox VE:
* use ip link for determining physical interfaces in the network configuration
stack
* move metric collection over to this new method as well
In the process I moved the existing functions for querying 'ip link', that were
already used by the firewall, to proxmox-network-api which seems like a better
fit than proxmox-ve-config (which was only a temporary solution anyway).
I also decided to move PBS over to the implementations contained in
proxmox-network-api, but if this is considered too drastic a change so close to
a release (I'm torn myself), then it should be trivial to revert those changes
and implement / copy-paste everything to the respective projects instead.
PBS does not automatically apply pending changes on reboot. It is already agreed
with @Thomas, that he will add a respective service to the PBS repository.
proxmox-backup depends on proxmox-network-api
proxmox-firewall depends on proxmox-network-api
proxmox-network-interface-pinning depends on proxmox-network-api
proxmox-ve-rs:
Stefan Hanreich (1):
host: network: move to proxmox-network-api
proxmox-ve-config/src/host/mod.rs | 1 -
proxmox-ve-config/src/host/network.rs | 35 ---------------------------
2 files changed, 36 deletions(-)
delete mode 100644 proxmox-ve-config/src/host/network.rs
proxmox:
Stefan Hanreich (3):
pbs-api-types: use proxmox-network-api types
proxmox-network-api: use ip link for querying interface information
network-api: add rename_interfaces method
Cargo.toml | 1 +
pbs-api-types/src/network.rs | 345 -----------------------
proxmox-network-api/Cargo.toml | 2 +
proxmox-network-api/debian/control | 8 +-
proxmox-network-api/src/api_types.rs | 8 +-
proxmox-network-api/src/config/helper.rs | 158 +++++++----
proxmox-network-api/src/config/mod.rs | 72 ++++-
proxmox-network-api/src/config/parser.rs | 37 ++-
8 files changed, 206 insertions(+), 425 deletions(-)
delete mode 100644 pbs-api-types/src/network.rs
proxmox-backup:
Stefan Hanreich (2):
config: network: move to proxmox-network-api
metric_collection: use ip link for determining the type of interfaces
Cargo.toml | 5 +
debian/control | 1 +
pbs-config/src/lib.rs | 10 +-
pbs-config/src/network/helper.rs | 223 -----
pbs-config/src/network/lexer.rs | 136 ---
pbs-config/src/network/mod.rs | 687 ---------------
pbs-config/src/network/parser.rs | 846 -------------------
src/api2/node/network.rs | 13 +-
src/bin/proxmox-backup-api.rs | 3 +-
src/bin/proxmox-backup-manager.rs | 1 +
src/bin/proxmox-backup-proxy.rs | 1 +
src/bin/proxmox_backup_manager/network.rs | 14 +-
src/server/metric_collection/mod.rs | 87 +-
src/server/metric_collection/pull_metrics.rs | 5 +-
src/server/metric_collection/rrd.rs | 5 +-
15 files changed, 112 insertions(+), 1925 deletions(-)
delete mode 100644 pbs-config/src/network/helper.rs
delete mode 100644 pbs-config/src/network/lexer.rs
delete mode 100644 pbs-config/src/network/mod.rs
delete mode 100644 pbs-config/src/network/parser.rs
proxmox-firewall:
Stefan Hanreich (1):
firewall: config: use proxmox-network-api
proxmox-firewall/Cargo.toml | 3 ++-
proxmox-firewall/src/config.rs | 29 +++++----------------
proxmox-firewall/tests/integration_tests.rs | 8 +++---
3 files changed, 12 insertions(+), 28 deletions(-)
proxmox-network-interface-pinning:
Stefan Hanreich (1):
initial commit
Summary over all repositories:
28 files changed, 330 insertions(+), 2414 deletions(-)
--
Generated by git-murpp 0.8.0
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
^ permalink raw reply [flat|nested] 16+ messages in thread
* [pbs-devel] [PATCH proxmox-ve-rs 1/1] host: network: move to proxmox-network-api
2025-07-29 16:56 [pbs-devel] [PATCH proxmox{-ve-rs, , -backup, -firewall, -network-interface-pinning} 0/8] proxmox-network-interface-pinning Stefan Hanreich
@ 2025-07-29 16:56 ` Stefan Hanreich
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox 1/3] pbs-api-types: use proxmox-network-api types Stefan Hanreich
` (7 subsequent siblings)
8 siblings, 0 replies; 16+ messages in thread
From: Stefan Hanreich @ 2025-07-29 16:56 UTC (permalink / raw)
To: pbs-devel
The structs have been moved to the proxmox-network-api, so they can be
reused across all projects. Initially it has been added here for
convenience, since the only user was proxmox-firewall - which is not
the case anymore.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-ve-config/src/host/mod.rs | 1 -
proxmox-ve-config/src/host/network.rs | 35 ---------------------------
2 files changed, 36 deletions(-)
delete mode 100644 proxmox-ve-config/src/host/network.rs
diff --git a/proxmox-ve-config/src/host/mod.rs b/proxmox-ve-config/src/host/mod.rs
index a9da919..b4ab6a6 100644
--- a/proxmox-ve-config/src/host/mod.rs
+++ b/proxmox-ve-config/src/host/mod.rs
@@ -1,3 +1,2 @@
-pub mod network;
pub mod types;
pub mod utils;
diff --git a/proxmox-ve-config/src/host/network.rs b/proxmox-ve-config/src/host/network.rs
deleted file mode 100644
index 09f4fb1..0000000
--- a/proxmox-ve-config/src/host/network.rs
+++ /dev/null
@@ -1,35 +0,0 @@
-use std::collections::HashMap;
-
-#[derive(Debug, Clone, serde::Deserialize)]
-pub struct IpLink {
- ifname: String,
- #[serde(default)]
- altnames: Vec<String>,
-}
-
-#[derive(Debug, Clone, serde::Deserialize)]
-pub struct InterfaceMapping {
- mapping: HashMap<String, String>,
-}
-
-impl std::ops::Deref for InterfaceMapping {
- type Target = HashMap<String, String>;
-
- fn deref(&self) -> &Self::Target {
- &self.mapping
- }
-}
-
-impl FromIterator<IpLink> for InterfaceMapping {
- fn from_iter<T: IntoIterator<Item = IpLink>>(iter: T) -> Self {
- let mut mapping = HashMap::new();
-
- for iface in iter.into_iter() {
- for altname in iface.altnames {
- mapping.insert(altname, iface.ifname.clone());
- }
- }
-
- Self { mapping }
- }
-}
--
2.47.2
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
^ permalink raw reply [flat|nested] 16+ messages in thread
* [pbs-devel] [PATCH proxmox 1/3] pbs-api-types: use proxmox-network-api types
2025-07-29 16:56 [pbs-devel] [PATCH proxmox{-ve-rs, , -backup, -firewall, -network-interface-pinning} 0/8] proxmox-network-interface-pinning Stefan Hanreich
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox-ve-rs 1/1] host: network: move to proxmox-network-api Stefan Hanreich
@ 2025-07-29 16:56 ` Stefan Hanreich
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox 2/3] proxmox-network-api: use ip link for querying interface information Stefan Hanreich
` (6 subsequent siblings)
8 siblings, 0 replies; 16+ messages in thread
From: Stefan Hanreich @ 2025-07-29 16:56 UTC (permalink / raw)
To: pbs-devel
proxmox-network-api contains the api types for the network API as
well. Since it has been migrated to use the types from
proxmox-network-api, we can delete them from the pbs specific crate.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
pbs-api-types/src/network.rs | 345 ---------------------------
proxmox-network-api/src/api_types.rs | 8 +-
2 files changed, 4 insertions(+), 349 deletions(-)
delete mode 100644 pbs-api-types/src/network.rs
diff --git a/pbs-api-types/src/network.rs b/pbs-api-types/src/network.rs
deleted file mode 100644
index fe083dc6..00000000
--- a/pbs-api-types/src/network.rs
+++ /dev/null
@@ -1,345 +0,0 @@
-use std::fmt;
-
-use serde::{Deserialize, Serialize};
-
-use proxmox_schema::*;
-
-use crate::{
- CIDR_FORMAT, CIDR_V4_FORMAT, CIDR_V6_FORMAT, IP_FORMAT, IP_V4_FORMAT, IP_V6_FORMAT,
- PROXMOX_SAFE_ID_REGEX,
-};
-
-pub const NETWORK_INTERFACE_FORMAT: ApiStringFormat =
- ApiStringFormat::Pattern(&PROXMOX_SAFE_ID_REGEX);
-
-pub const IP_V4_SCHEMA: Schema = StringSchema::new("IPv4 address.")
- .format(&IP_V4_FORMAT)
- .max_length(15)
- .schema();
-
-pub const IP_V6_SCHEMA: Schema = StringSchema::new("IPv6 address.")
- .format(&IP_V6_FORMAT)
- .max_length(39)
- .schema();
-
-pub const IP_SCHEMA: Schema = StringSchema::new("IP (IPv4 or IPv6) address.")
- .format(&IP_FORMAT)
- .max_length(39)
- .schema();
-
-pub const CIDR_V4_SCHEMA: Schema = StringSchema::new("IPv4 address with netmask (CIDR notation).")
- .format(&CIDR_V4_FORMAT)
- .max_length(18)
- .schema();
-
-pub const CIDR_V6_SCHEMA: Schema = StringSchema::new("IPv6 address with netmask (CIDR notation).")
- .format(&CIDR_V6_FORMAT)
- .max_length(43)
- .schema();
-
-pub const CIDR_SCHEMA: Schema =
- StringSchema::new("IP address (IPv4 or IPv6) with netmask (CIDR notation).")
- .format(&CIDR_FORMAT)
- .max_length(43)
- .schema();
-
-#[api()]
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "lowercase")]
-/// Interface configuration method
-pub enum NetworkConfigMethod {
- /// Configuration is done manually using other tools
- Manual,
- /// Define interfaces with statically allocated addresses.
- Static,
- /// Obtain an address via DHCP
- DHCP,
- /// Define the loopback interface.
- Loopback,
-}
-
-#[api()]
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case")]
-#[repr(u8)]
-/// Linux Bond Mode
-pub enum LinuxBondMode {
- /// Round-robin policy
- BalanceRr = 0,
- /// Active-backup policy
- ActiveBackup = 1,
- /// XOR policy
- BalanceXor = 2,
- /// Broadcast policy
- Broadcast = 3,
- /// IEEE 802.3ad Dynamic link aggregation
- #[serde(rename = "802.3ad")]
- Ieee802_3ad = 4,
- /// Adaptive transmit load balancing
- BalanceTlb = 5,
- /// Adaptive load balancing
- BalanceAlb = 6,
-}
-
-impl fmt::Display for LinuxBondMode {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- f.write_str(match self {
- LinuxBondMode::BalanceRr => "balance-rr",
- LinuxBondMode::ActiveBackup => "active-backup",
- LinuxBondMode::BalanceXor => "balance-xor",
- LinuxBondMode::Broadcast => "broadcast",
- LinuxBondMode::Ieee802_3ad => "802.3ad",
- LinuxBondMode::BalanceTlb => "balance-tlb",
- LinuxBondMode::BalanceAlb => "balance-alb",
- })
- }
-}
-
-#[api()]
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case")]
-#[repr(u8)]
-/// Bond Transmit Hash Policy for LACP (802.3ad)
-pub enum BondXmitHashPolicy {
- /// Layer 2
- Layer2 = 0,
- /// Layer 2+3
- #[serde(rename = "layer2+3")]
- Layer2_3 = 1,
- /// Layer 3+4
- #[serde(rename = "layer3+4")]
- Layer3_4 = 2,
-}
-
-impl fmt::Display for BondXmitHashPolicy {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- f.write_str(match self {
- BondXmitHashPolicy::Layer2 => "layer2",
- BondXmitHashPolicy::Layer2_3 => "layer2+3",
- BondXmitHashPolicy::Layer3_4 => "layer3+4",
- })
- }
-}
-
-#[api()]
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "lowercase")]
-/// Network interface type
-pub enum NetworkInterfaceType {
- /// Loopback
- Loopback,
- /// Physical Ethernet device
- Eth,
- /// Linux Bridge
- Bridge,
- /// Linux Bond
- Bond,
- /// Linux VLAN (eth.10)
- Vlan,
- /// Interface Alias (eth:1)
- Alias,
- /// Unknown interface type
- Unknown,
-}
-
-pub const NETWORK_INTERFACE_NAME_SCHEMA: Schema = StringSchema::new("Network interface name.")
- .format(&NETWORK_INTERFACE_FORMAT)
- .min_length(1)
- .max_length(15) // libc::IFNAMSIZ-1
- .schema();
-
-pub const NETWORK_INTERFACE_ARRAY_SCHEMA: Schema =
- ArraySchema::new("Network interface list.", &NETWORK_INTERFACE_NAME_SCHEMA).schema();
-
-pub const NETWORK_INTERFACE_LIST_SCHEMA: Schema =
- StringSchema::new("A list of network devices, comma separated.")
- .format(&ApiStringFormat::PropertyString(
- &NETWORK_INTERFACE_ARRAY_SCHEMA,
- ))
- .schema();
-
-#[api(
- properties: {
- name: {
- schema: NETWORK_INTERFACE_NAME_SCHEMA,
- },
- "type": {
- type: NetworkInterfaceType,
- },
- method: {
- type: NetworkConfigMethod,
- optional: true,
- },
- method6: {
- type: NetworkConfigMethod,
- optional: true,
- },
- cidr: {
- schema: CIDR_V4_SCHEMA,
- optional: true,
- },
- cidr6: {
- schema: CIDR_V6_SCHEMA,
- optional: true,
- },
- gateway: {
- schema: IP_V4_SCHEMA,
- optional: true,
- },
- gateway6: {
- schema: IP_V6_SCHEMA,
- optional: true,
- },
- options: {
- description: "Option list (inet)",
- type: Array,
- items: {
- description: "Optional attribute line.",
- type: String,
- },
- },
- options6: {
- description: "Option list (inet6)",
- type: Array,
- items: {
- description: "Optional attribute line.",
- type: String,
- },
- },
- comments: {
- description: "Comments (inet, may span multiple lines)",
- type: String,
- optional: true,
- },
- comments6: {
- description: "Comments (inet6, may span multiple lines)",
- type: String,
- optional: true,
- },
- bridge_ports: {
- schema: NETWORK_INTERFACE_ARRAY_SCHEMA,
- optional: true,
- },
- slaves: {
- schema: NETWORK_INTERFACE_ARRAY_SCHEMA,
- optional: true,
- },
- "vlan-id": {
- description: "VLAN ID.",
- type: u16,
- optional: true,
- },
- "vlan-raw-device": {
- schema: NETWORK_INTERFACE_NAME_SCHEMA,
- optional: true,
- },
- bond_mode: {
- type: LinuxBondMode,
- optional: true,
- },
- "bond-primary": {
- schema: NETWORK_INTERFACE_NAME_SCHEMA,
- optional: true,
- },
- bond_xmit_hash_policy: {
- type: BondXmitHashPolicy,
- optional: true,
- },
- }
-)]
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
-/// Network Interface configuration
-pub struct Interface {
- /// Autostart interface
- #[serde(rename = "autostart")]
- pub autostart: bool,
- /// Interface is active (UP)
- pub active: bool,
- /// Interface name
- pub name: String,
- /// Interface type
- #[serde(rename = "type")]
- pub interface_type: NetworkInterfaceType,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub method: Option<NetworkConfigMethod>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub method6: Option<NetworkConfigMethod>,
- #[serde(skip_serializing_if = "Option::is_none")]
- /// IPv4 address with netmask
- pub cidr: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- /// IPv4 gateway
- pub gateway: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- /// IPv6 address with netmask
- pub cidr6: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- /// IPv6 gateway
- pub gateway6: Option<String>,
-
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub options: Vec<String>,
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub options6: Vec<String>,
-
- #[serde(skip_serializing_if = "Option::is_none")]
- pub comments: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub comments6: Option<String>,
-
- #[serde(skip_serializing_if = "Option::is_none")]
- /// Maximum Transmission Unit
- pub mtu: Option<u64>,
-
- #[serde(skip_serializing_if = "Option::is_none")]
- pub bridge_ports: Option<Vec<String>>,
- /// Enable bridge vlan support.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub bridge_vlan_aware: Option<bool>,
- #[serde(skip_serializing_if = "Option::is_none")]
- #[serde(rename = "vlan-id")]
- pub vlan_id: Option<u16>,
- #[serde(skip_serializing_if = "Option::is_none")]
- #[serde(rename = "vlan-raw-device")]
- pub vlan_raw_device: Option<String>,
-
- #[serde(skip_serializing_if = "Option::is_none")]
- pub slaves: Option<Vec<String>>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub bond_mode: Option<LinuxBondMode>,
- #[serde(skip_serializing_if = "Option::is_none")]
- #[serde(rename = "bond-primary")]
- pub bond_primary: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub bond_xmit_hash_policy: Option<BondXmitHashPolicy>,
-}
-
-impl Interface {
- pub fn new(name: String) -> Self {
- Self {
- name,
- interface_type: NetworkInterfaceType::Unknown,
- autostart: false,
- active: false,
- method: None,
- method6: None,
- cidr: None,
- gateway: None,
- cidr6: None,
- gateway6: None,
- options: Vec::new(),
- options6: Vec::new(),
- comments: None,
- comments6: None,
- mtu: None,
- bridge_ports: None,
- bridge_vlan_aware: None,
- vlan_id: None,
- vlan_raw_device: None,
- slaves: None,
- bond_mode: None,
- bond_primary: None,
- bond_xmit_hash_policy: None,
- }
- }
-}
diff --git a/proxmox-network-api/src/api_types.rs b/proxmox-network-api/src/api_types.rs
index 9f12b029..00c941e5 100644
--- a/proxmox-network-api/src/api_types.rs
+++ b/proxmox-network-api/src/api_types.rs
@@ -6,10 +6,10 @@ use serde::{Deserialize, Serialize};
use regex::Regex;
-use proxmox_schema::api;
-use proxmox_schema::api_types::SAFE_ID_REGEX;
-use proxmox_schema::api_types::{CIDR_V4_SCHEMA, CIDR_V6_SCHEMA};
-use proxmox_schema::api_types::{IP_V4_SCHEMA, IP_V6_SCHEMA};
+pub use proxmox_schema::api;
+pub use proxmox_schema::api_types::SAFE_ID_REGEX;
+pub use proxmox_schema::api_types::{CIDR_V4_SCHEMA, CIDR_V6_SCHEMA};
+pub use proxmox_schema::api_types::{IP_V4_SCHEMA, IP_V6_SCHEMA};
use proxmox_schema::ApiStringFormat;
use proxmox_schema::ArraySchema;
use proxmox_schema::Schema;
--
2.47.2
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
^ permalink raw reply [flat|nested] 16+ messages in thread
* [pbs-devel] [PATCH proxmox 2/3] proxmox-network-api: use ip link for querying interface information
2025-07-29 16:56 [pbs-devel] [PATCH proxmox{-ve-rs, , -backup, -firewall, -network-interface-pinning} 0/8] proxmox-network-interface-pinning Stefan Hanreich
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox-ve-rs 1/1] host: network: move to proxmox-network-api Stefan Hanreich
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox 1/3] pbs-api-types: use proxmox-network-api types Stefan Hanreich
@ 2025-07-29 16:56 ` Stefan Hanreich
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox 3/3] network-api: add rename_interfaces method Stefan Hanreich
` (5 subsequent siblings)
8 siblings, 0 replies; 16+ messages in thread
From: Stefan Hanreich @ 2025-07-29 16:56 UTC (permalink / raw)
To: pbs-devel
We only obtained information about whether an interface is active or
not when querying the interfaces of a host. This is not sufficient
anymore with the introduction of proxmox-network-interface-pinning. We
need additional information (MAC address, type of interface, altnames)
to pin network interfaces and to detect them in the rest of the PBS
network stack.
Use 'ip link', analogous to Proxmox VE, instead which provides all the
required information. In the future this could be adapted to query the
kernel via netlink directly, avoiding spawning an additional process.
For this reason, the struct does not expose any internals, so we can
easily switch to netlink in a transparent way for all call sites.
Part of this has been copied over from proxmox-ve-rs, which already
had versions of the IpLink and AltnameMapping struct.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
Cargo.toml | 1 +
proxmox-network-api/Cargo.toml | 2 +
proxmox-network-api/debian/control | 8 +-
proxmox-network-api/src/config/helper.rs | 158 ++++++++++++++---------
proxmox-network-api/src/config/mod.rs | 4 +-
proxmox-network-api/src/config/parser.rs | 37 ++++--
6 files changed, 134 insertions(+), 76 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index fd7eba63..a3d185c6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -148,6 +148,7 @@ proxmox-io = { version = "1.2.0", path = "proxmox-io" }
proxmox-lang = { version = "1.5", path = "proxmox-lang" }
proxmox-log = { version = "1.0.0", path = "proxmox-log" }
proxmox-login = { version = "1.0.0", path = "proxmox-login" }
+proxmox-network-types = { version = "0.1.0", path = "proxmox-network-types" }
proxmox-product-config = { version = "1.0.0", path = "proxmox-product-config" }
proxmox-config-digest = { version = "1.0.0", path = "proxmox-config-digest" }
proxmox-rest-server = { version = "1.0.0", path = "proxmox-rest-server" }
diff --git a/proxmox-network-api/Cargo.toml b/proxmox-network-api/Cargo.toml
index add3018b..b4b7db9e 100644
--- a/proxmox-network-api/Cargo.toml
+++ b/proxmox-network-api/Cargo.toml
@@ -17,6 +17,7 @@ const_format.workspace = true
regex.workspace = true
serde = { workspace = true, features = ["derive"] }
+serde_json.workspace = true
nix = { workspace = true, optional = true }
libc = { workspace = true, optional = true }
@@ -24,6 +25,7 @@ proxmox-sys = { workspace = true, optional = true }
proxmox-schema = { workspace = true, features = ["api-macro", "api-types"] }
proxmox-config-digest = { workspace = true, optional = true }
proxmox-product-config = { workspace = true, optional = true }
+proxmox-network-types.workspace = true
[features]
default = []
diff --git a/proxmox-network-api/debian/control b/proxmox-network-api/debian/control
index dcdad532..581db711 100644
--- a/proxmox-network-api/debian/control
+++ b/proxmox-network-api/debian/control
@@ -8,12 +8,14 @@ Build-Depends-Arch: cargo:native <!nocheck>,
libstd-rust-dev <!nocheck>,
librust-anyhow-1+default-dev <!nocheck>,
librust-const-format-0.2+default-dev <!nocheck>,
+ librust-proxmox-network-types-0.1+default-dev <!nocheck>,
librust-proxmox-schema-4+api-macro-dev (>= 4.1.0-~~) <!nocheck>,
librust-proxmox-schema-4+api-types-dev (>= 4.1.0-~~) <!nocheck>,
librust-proxmox-schema-4+default-dev (>= 4.1.0-~~) <!nocheck>,
librust-regex-1+default-dev (>= 1.5-~~) <!nocheck>,
librust-serde-1+default-dev <!nocheck>,
- librust-serde-1+derive-dev <!nocheck>
+ librust-serde-1+derive-dev <!nocheck>,
+ librust-serde-json-1+default-dev <!nocheck>
Maintainer: Proxmox Support Team <support@proxmox.com>
Standards-Version: 4.7.0
Vcs-Git: git://git.proxmox.com/git/proxmox.git
@@ -29,12 +31,14 @@ Depends:
${misc:Depends},
librust-anyhow-1+default-dev,
librust-const-format-0.2+default-dev,
+ librust-proxmox-network-types-0.1+default-dev,
librust-proxmox-schema-4+api-macro-dev (>= 4.1.0-~~),
librust-proxmox-schema-4+api-types-dev (>= 4.1.0-~~),
librust-proxmox-schema-4+default-dev (>= 4.1.0-~~),
librust-regex-1+default-dev (>= 1.5-~~),
librust-serde-1+default-dev,
- librust-serde-1+derive-dev
+ librust-serde-1+derive-dev,
+ librust-serde-json-1+default-dev
Suggests:
librust-proxmox-network-api+impl-dev (= ${binary:Version})
Provides:
diff --git a/proxmox-network-api/src/config/helper.rs b/proxmox-network-api/src/config/helper.rs
index 9d817c43..fa8a64de 100644
--- a/proxmox-network-api/src/config/helper.rs
+++ b/proxmox-network-api/src/config/helper.rs
@@ -1,15 +1,13 @@
use std::collections::HashMap;
-use std::os::unix::io::AsRawFd;
use std::path::Path;
use std::process::Command;
use std::sync::LazyLock;
-use anyhow::{bail, format_err, Error};
+use anyhow::{bail, format_err, Context, Error};
use const_format::concatcp;
-use nix::ioctl_read_bad;
-use nix::sys::socket::{socket, AddressFamily, SockFlag, SockType};
use regex::Regex;
+use proxmox_network_types::mac_address::MacAddress;
use proxmox_schema::api_types::IPV4RE_STR;
use proxmox_schema::api_types::IPV6RE_STR;
@@ -119,72 +117,110 @@ pub(crate) fn parse_address_or_cidr(cidr: &str) -> Result<(String, Option<u8>, b
}
}
-pub(crate) fn get_network_interfaces() -> Result<HashMap<String, bool>, Error> {
- const PROC_NET_DEV: &str = "/proc/net/dev";
+#[derive(Debug, Clone, serde::Deserialize)]
+pub struct SlaveData {
+ perm_hw_addr: Option<MacAddress>,
+}
+
+#[derive(Debug, Clone, serde::Deserialize)]
+pub struct LinkInfo {
+ info_slave_data: Option<SlaveData>,
+ info_kind: Option<String>,
+}
+
+#[derive(Debug, Clone, serde::Deserialize)]
+pub struct IpLink {
+ ifname: String,
+ #[serde(default)]
+ altnames: Vec<String>,
+ ifindex: i64,
+ link_type: String,
+ address: MacAddress,
+ linkinfo: Option<LinkInfo>,
+ operstate: String,
+}
+
+impl IpLink {
+ pub fn index(&self) -> i64 {
+ self.ifindex
+ }
+
+ pub fn is_physical(&self) -> bool {
+ self.link_type == "ether"
+ && (self.linkinfo.is_none() || self.linkinfo.as_ref().unwrap().info_kind.is_none())
+ }
- #[repr(C)]
- pub struct ifreq {
- ifr_name: [libc::c_uchar; libc::IFNAMSIZ],
- ifru_flags: libc::c_short,
+ pub fn name(&self) -> &str {
+ &self.ifname
}
- ioctl_read_bad!(get_interface_flags, libc::SIOCGIFFLAGS, ifreq);
-
- static IFACE_LINE_REGEX: LazyLock<Regex> =
- LazyLock::new(|| Regex::new(r"^\s*([^:\s]+):").unwrap());
- let raw = std::fs::read_to_string(PROC_NET_DEV)
- .map_err(|err| format_err!("unable to read {} - {}", PROC_NET_DEV, err))?;
-
- let lines = raw.lines();
-
- let sock = socket(
- AddressFamily::Inet,
- SockType::Datagram,
- SockFlag::empty(),
- None,
- )
- .or_else(|_| {
- socket(
- AddressFamily::Inet6,
- SockType::Datagram,
- SockFlag::empty(),
- None,
- )
- })?;
-
- let mut interface_list = HashMap::new();
-
- for line in lines {
- if let Some(cap) = IFACE_LINE_REGEX.captures(line) {
- let ifname = &cap[1];
-
- let mut req = ifreq {
- ifr_name: *b"0000000000000000",
- ifru_flags: 0,
- };
- for (i, b) in std::ffi::CString::new(ifname)?
- .as_bytes_with_nul()
- .iter()
- .enumerate()
- {
- if i < (libc::IFNAMSIZ - 1) {
- req.ifr_name[i] = *b as libc::c_uchar;
+ pub fn permanent_mac(&self) -> MacAddress {
+ if let Some(link_info) = &self.linkinfo {
+ if let Some(info_slave_data) = &link_info.info_slave_data {
+ if let Some(perm_hw_addr) = info_slave_data.perm_hw_addr {
+ return perm_hw_addr;
}
}
- let res = unsafe { get_interface_flags(sock.as_raw_fd(), &mut req)? };
- if res != 0 {
- bail!(
- "ioctl get_interface_flags for '{}' failed ({})",
- ifname,
- res
- );
+ }
+
+ self.address
+ }
+
+ pub fn altnames(&self) -> impl Iterator<Item = &String> {
+ self.altnames.iter()
+ }
+
+ pub fn active(&self) -> bool {
+ self.operstate == "UP"
+ }
+}
+
+#[derive(Debug, Clone, serde::Deserialize)]
+pub struct AltnameMapping {
+ mapping: HashMap<String, String>,
+}
+
+impl std::ops::Deref for AltnameMapping {
+ type Target = HashMap<String, String>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.mapping
+ }
+}
+
+impl FromIterator<IpLink> for AltnameMapping {
+ fn from_iter<T: IntoIterator<Item = IpLink>>(iter: T) -> Self {
+ let mut mapping = HashMap::new();
+
+ for iface in iter.into_iter() {
+ for altname in iface.altnames {
+ mapping.insert(altname, iface.ifname.clone());
}
- let is_up = (req.ifru_flags & (libc::IFF_UP as libc::c_short)) != 0;
- interface_list.insert(ifname.to_string(), is_up);
}
+
+ Self { mapping }
+ }
+}
+
+pub fn get_network_interfaces() -> Result<HashMap<String, IpLink>, Error> {
+ let output = std::process::Command::new("ip")
+ .arg("-details")
+ .arg("-json")
+ .arg("link")
+ .arg("show")
+ .stdout(std::process::Stdio::piped())
+ .output()
+ .with_context(|| "could not obtain ip link output")?;
+
+ if !output.status.success() {
+ bail!("ip link returned non-zero exit code")
}
- Ok(interface_list)
+ Ok(serde_json::from_slice::<Vec<IpLink>>(&output.stdout)
+ .with_context(|| "could not deserialize ip link output")?
+ .into_iter()
+ .map(|ip_link| (ip_link.ifname.clone(), ip_link))
+ .collect())
}
pub(crate) fn compute_file_diff(filename: &str, shadow: &str) -> Result<String, Error> {
diff --git a/proxmox-network-api/src/config/mod.rs b/proxmox-network-api/src/config/mod.rs
index 054f53c8..e8cb81d1 100644
--- a/proxmox-network-api/src/config/mod.rs
+++ b/proxmox-network-api/src/config/mod.rs
@@ -2,7 +2,7 @@ mod helper;
mod lexer;
mod parser;
-pub use helper::{assert_ifupdown2_installed, network_reload, parse_cidr};
+pub use helper::{assert_ifupdown2_installed, network_reload, parse_cidr, AltnameMapping, IpLink};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::io::Write;
@@ -17,7 +17,7 @@ use super::{
};
use helper::compute_file_diff;
-use helper::get_network_interfaces;
+pub use helper::get_network_interfaces;
use parser::NetworkParser;
use proxmox_config_digest::ConfigDigest;
diff --git a/proxmox-network-api/src/config/parser.rs b/proxmox-network-api/src/config/parser.rs
index d05a67b0..500ae8ec 100644
--- a/proxmox-network-api/src/config/parser.rs
+++ b/proxmox-network-api/src/config/parser.rs
@@ -1,4 +1,4 @@
-use crate::VLAN_INTERFACE_REGEX;
+use crate::{PHYSICAL_NIC_REGEX, VLAN_INTERFACE_REGEX};
use std::collections::{HashMap, HashSet};
use std::io::BufRead;
@@ -502,7 +502,7 @@ impl<R: BufRead> NetworkParser<R> {
pub fn parse_interfaces(
&mut self,
- existing_interfaces: Option<&HashMap<String, bool>>,
+ existing_interfaces: Option<&HashMap<String, IpLink>>,
) -> Result<NetworkConfig, Error> {
self.do_parse_interfaces(existing_interfaces)
.map_err(|err| format_err!("line {}: {}", self.line_nr, err))
@@ -510,7 +510,7 @@ impl<R: BufRead> NetworkParser<R> {
fn do_parse_interfaces(
&mut self,
- existing_interfaces: Option<&HashMap<String, bool>>,
+ existing_interfaces: Option<&HashMap<String, IpLink>>,
) -> Result<NetworkConfig, Error> {
let mut config = NetworkConfig::new();
@@ -555,20 +555,20 @@ impl<R: BufRead> NetworkParser<R> {
LazyLock::new(|| Regex::new(r"^\S+:\d+$").unwrap());
if let Some(existing_interfaces) = existing_interfaces {
- for (iface, active) in existing_interfaces.iter() {
+ for (iface, ip_link) in existing_interfaces.iter() {
if let Some(interface) = config.interfaces.get_mut(iface) {
- interface.active = *active;
+ interface.active = ip_link.active();
if interface.interface_type == NetworkInterfaceType::Unknown
- && super::is_physical_nic(iface)
+ && ip_link.is_physical()
{
interface.interface_type = NetworkInterfaceType::Eth;
}
- } else if super::is_physical_nic(iface) {
+ } else if ip_link.is_physical() {
// also add all physical NICs
let mut interface = Interface::new(iface.clone());
set_method_v4(&mut interface, NetworkConfigMethod::Manual)?;
interface.interface_type = NetworkInterfaceType::Eth;
- interface.active = *active;
+ interface.active = ip_link.active();
config.interfaces.insert(interface.name.clone(), interface);
config
.order
@@ -593,9 +593,24 @@ impl<R: BufRead> NetworkParser<R> {
interface.interface_type = NetworkInterfaceType::Vlan;
continue;
}
- if super::is_physical_nic(name) {
- interface.interface_type = NetworkInterfaceType::Eth;
- continue;
+
+ match existing_interfaces {
+ Some(existing_interfaces) => {
+ let is_physical = existing_interfaces
+ .get(name)
+ .map(|iface| iface.is_physical())
+ .unwrap_or(false);
+
+ if is_physical {
+ interface.interface_type = NetworkInterfaceType::Eth;
+ continue;
+ }
+ }
+ None if PHYSICAL_NIC_REGEX.is_match(name) => {
+ interface.interface_type = NetworkInterfaceType::Eth;
+ continue;
+ }
+ _ => {}
}
}
--
2.47.2
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
^ permalink raw reply [flat|nested] 16+ messages in thread
* [pbs-devel] [PATCH proxmox 3/3] network-api: add rename_interfaces method
2025-07-29 16:56 [pbs-devel] [PATCH proxmox{-ve-rs, , -backup, -firewall, -network-interface-pinning} 0/8] proxmox-network-interface-pinning Stefan Hanreich
` (2 preceding siblings ...)
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox 2/3] proxmox-network-api: use ip link for querying interface information Stefan Hanreich
@ 2025-07-29 16:56 ` Stefan Hanreich
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox-backup 1/2] config: network: move to proxmox-network-api Stefan Hanreich
` (4 subsequent siblings)
8 siblings, 0 replies; 16+ messages in thread
From: Stefan Hanreich @ 2025-07-29 16:56 UTC (permalink / raw)
To: pbs-devel
Used for batch renaming interfaces in the /e/n/i configuration file by
the proxmox-network-interface-pinning tool.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-network-api/src/config/mod.rs | 68 +++++++++++++++++++++++++++
1 file changed, 68 insertions(+)
diff --git a/proxmox-network-api/src/config/mod.rs b/proxmox-network-api/src/config/mod.rs
index e8cb81d1..3b2ffdc2 100644
--- a/proxmox-network-api/src/config/mod.rs
+++ b/proxmox-network-api/src/config/mod.rs
@@ -267,6 +267,74 @@ impl NetworkConfig {
Ok(interface)
}
+ pub fn rename_interfaces(&mut self, mapping: &HashMap<String, String>) -> Result<(), Error> {
+ for (old_name, new_name) in mapping.iter() {
+ self.interfaces
+ .remove(old_name)
+ .map(|interface| self.interfaces.insert(new_name.to_string(), interface));
+
+ if let Some(idx) = self
+ .order
+ .iter()
+ .position(|elem| matches!(elem, NetworkOrderEntry::Iface(name) if name == new_name))
+ {
+ self.order[idx] = NetworkOrderEntry::Iface(new_name.to_string());
+ }
+ }
+
+ for interface in self.interfaces.values_mut() {
+ if let Some(new_name) = mapping.get(&interface.name) {
+ interface.name = new_name.to_string();
+ }
+
+ if let Some(bridge_ports) = interface.bridge_ports.take() {
+ interface.bridge_ports = Some(
+ bridge_ports
+ .into_iter()
+ .map(|interface| {
+ mapping
+ .get(&interface)
+ .map(String::from)
+ .unwrap_or(interface)
+ })
+ .collect(),
+ )
+ }
+
+ if let Some(vlan_raw_device) = interface.vlan_raw_device.take() {
+ if let Some(new_name) = mapping.get(&vlan_raw_device) {
+ interface.vlan_raw_device = Some(new_name.to_string());
+ } else {
+ interface.vlan_raw_device = Some(vlan_raw_device);
+ }
+ }
+
+ if let Some(slaves) = interface.slaves.take() {
+ interface.slaves = Some(
+ slaves
+ .into_iter()
+ .map(|interface| {
+ mapping
+ .get(&interface)
+ .map(String::from)
+ .unwrap_or(interface)
+ })
+ .collect(),
+ )
+ }
+
+ if let Some(bond_primary) = interface.bond_primary.take() {
+ if let Some(new_name) = mapping.get(&bond_primary) {
+ interface.bond_primary = Some(new_name.to_string());
+ } else {
+ interface.bond_primary = Some(bond_primary);
+ }
+ }
+ }
+
+ Ok(())
+ }
+
/// Check that there is no other gateway.
///
/// The gateway property is only allowed on passed 'iface'. This should be
--
2.47.2
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
^ permalink raw reply [flat|nested] 16+ messages in thread
* [pbs-devel] [PATCH proxmox-backup 1/2] config: network: move to proxmox-network-api
2025-07-29 16:56 [pbs-devel] [PATCH proxmox{-ve-rs, , -backup, -firewall, -network-interface-pinning} 0/8] proxmox-network-interface-pinning Stefan Hanreich
` (3 preceding siblings ...)
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox 3/3] network-api: add rename_interfaces method Stefan Hanreich
@ 2025-07-29 16:56 ` Stefan Hanreich
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox-backup 2/2] metric_collection: use ip link for determining the type of interfaces Stefan Hanreich
` (3 subsequent siblings)
8 siblings, 0 replies; 16+ messages in thread
From: Stefan Hanreich @ 2025-07-29 16:56 UTC (permalink / raw)
To: pbs-devel
With the introduction of PDM, the network part has been extracted into
its own crate. Adapt proxmox-backup to use the proxmox-network-api
crate as well.
proxmox-network-api has additional commits compared to pbs-config, but
they should not change anything in the functionality for PBS so moving
to proxmox-network-api is fine.
proxmox-network-api uses a generic ApiLockGuard, instead of
BackupLockGuard, but the network configuration is locked by the root
user only in PBS (API endpoints are protected), so this should not be
an issue.
Other than that proxmox-network-api introduced new helpers for bond
modes and checking for duplicate gateways, as well as changed the
visibility of some functions (which weren't used in pbs-config call
sites).
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
Cargo.toml | 5 +
debian/control | 1 +
pbs-config/src/lib.rs | 10 +-
pbs-config/src/network/helper.rs | 223 ------
pbs-config/src/network/lexer.rs | 136 ----
pbs-config/src/network/mod.rs | 687 ------------------
pbs-config/src/network/parser.rs | 846 ----------------------
src/api2/node/network.rs | 13 +-
src/bin/proxmox-backup-api.rs | 3 +-
src/bin/proxmox-backup-manager.rs | 1 +
src/bin/proxmox-backup-proxy.rs | 1 +
src/bin/proxmox_backup_manager/network.rs | 14 +-
12 files changed, 33 insertions(+), 1907 deletions(-)
delete mode 100644 pbs-config/src/network/helper.rs
delete mode 100644 pbs-config/src/network/lexer.rs
delete mode 100644 pbs-config/src/network/mod.rs
delete mode 100644 pbs-config/src/network/parser.rs
diff --git a/Cargo.toml b/Cargo.toml
index c339e675..b6bc2bdf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -70,8 +70,10 @@ proxmox-lang = "1.1"
proxmox-log = "1"
proxmox-ldap = "1"
proxmox-metrics = "1"
+proxmox-network-api = "1"
proxmox-notify = "1"
proxmox-openid = "1"
+proxmox-product-config = "1"
proxmox-rest-server = { version = "1.0.1", features = [ "templates" ] }
# some use "cli", some use "cli" and "server", pbs-config uses nothing
proxmox-router = { version = "3.2.2", default-features = false }
@@ -221,8 +223,10 @@ proxmox-lang.workspace = true
proxmox-log.workspace = true
proxmox-ldap.workspace = true
proxmox-metrics.workspace = true
+proxmox-network-api = { workspace = true, features = [ "impl" ] }
proxmox-notify = { workspace = true, features = [ "pbs-context" ] }
proxmox-openid.workspace = true
+proxmox-product-config.workspace = true
proxmox-rest-server = { workspace = true, features = [ "rate-limited-stream" ] }
proxmox-router = { workspace = true, features = [ "cli", "server"] }
proxmox-s3-client.workspace = true
@@ -278,6 +282,7 @@ proxmox-rrd-api-types.workspace = true
#proxmox-log = { path = "../proxmox/proxmox-log" }
#proxmox-ldap = { path = "../proxmox/proxmox-ldap" }
#proxmox-metrics = { path = "../proxmox/proxmox-metrics" }
+#proxmox-network-api = { path = "../proxmox/proxmox-network-api" }
#proxmox-notify = { path = "../proxmox/proxmox-notify" }
#proxmox-openid = { path = "../proxmox/proxmox-openid" }
#proxmox-rest-server = { path = "../proxmox/proxmox-rest-server" }
diff --git a/debian/control b/debian/control
index 052cb519..61311c1a 100644
--- a/debian/control
+++ b/debian/control
@@ -93,6 +93,7 @@ Build-Depends: bash-completion,
librust-proxmox-notify-1+default-dev,
librust-proxmox-notify-1+pbs-context-dev,
librust-proxmox-openid-1+default-dev,
+ librust-proxmox-product-config-1+default-dev,
librust-proxmox-rest-server-1+default-dev (>= 1.0.1),
librust-proxmox-rest-server-1+rate-limited-stream-dev,
librust-proxmox-rest-server-1+templates-dev,
diff --git a/pbs-config/src/lib.rs b/pbs-config/src/lib.rs
index d03c079a..1ed47238 100644
--- a/pbs-config/src/lib.rs
+++ b/pbs-config/src/lib.rs
@@ -6,7 +6,6 @@ pub mod domains;
pub mod drive;
pub mod media_pool;
pub mod metrics;
-pub mod network;
pub mod notifications;
pub mod prune;
pub mod remote;
@@ -48,6 +47,15 @@ pub fn backup_group() -> Result<nix::unistd::Group, Error> {
}
}
+/// Return User info for root
+pub fn priv_user() -> Result<nix::unistd::User, Error> {
+ if cfg!(test) {
+ Ok(User::from_uid(Uid::current())?.expect("current user does not exist"))
+ } else {
+ User::from_name("root")?.ok_or_else(|| format_err!("Unable to lookup superuser."))
+ }
+}
+
pub struct BackupLockGuard {
file: Option<std::fs::File>,
// TODO: Remove `_legacy_dir` with PBS 5
diff --git a/pbs-config/src/network/helper.rs b/pbs-config/src/network/helper.rs
deleted file mode 100644
index e0b07a01..00000000
--- a/pbs-config/src/network/helper.rs
+++ /dev/null
@@ -1,223 +0,0 @@
-use std::collections::HashMap;
-use std::os::unix::io::AsRawFd;
-use std::path::Path;
-use std::process::Command;
-use std::sync::LazyLock;
-
-use anyhow::{bail, format_err, Error};
-use const_format::concatcp;
-use nix::ioctl_read_bad;
-use nix::sys::socket::{socket, AddressFamily, SockFlag, SockType};
-use regex::Regex;
-
-use pbs_api_types::*; // for IP macros
-
-pub static IPV4_REVERSE_MASK: &[&str] = &[
- "0.0.0.0",
- "128.0.0.0",
- "192.0.0.0",
- "224.0.0.0",
- "240.0.0.0",
- "248.0.0.0",
- "252.0.0.0",
- "254.0.0.0",
- "255.0.0.0",
- "255.128.0.0",
- "255.192.0.0",
- "255.224.0.0",
- "255.240.0.0",
- "255.248.0.0",
- "255.252.0.0",
- "255.254.0.0",
- "255.255.0.0",
- "255.255.128.0",
- "255.255.192.0",
- "255.255.224.0",
- "255.255.240.0",
- "255.255.248.0",
- "255.255.252.0",
- "255.255.254.0",
- "255.255.255.0",
- "255.255.255.128",
- "255.255.255.192",
- "255.255.255.224",
- "255.255.255.240",
- "255.255.255.248",
- "255.255.255.252",
- "255.255.255.254",
- "255.255.255.255",
-];
-
-pub static IPV4_MASK_HASH_LOCALNET: LazyLock<HashMap<&'static str, u8>> = LazyLock::new(|| {
- let mut map = HashMap::new();
- #[allow(clippy::needless_range_loop)]
- for i in 0..IPV4_REVERSE_MASK.len() {
- map.insert(IPV4_REVERSE_MASK[i], i as u8);
- }
- map
-});
-
-pub fn parse_cidr(cidr: &str) -> Result<(String, u8, bool), Error> {
- let (address, mask, is_v6) = parse_address_or_cidr(cidr)?;
- if let Some(mask) = mask {
- Ok((address, mask, is_v6))
- } else {
- bail!("missing netmask in '{}'", cidr);
- }
-}
-
-pub fn check_netmask(mask: u8, is_v6: bool) -> Result<(), Error> {
- let (ver, min, max) = if is_v6 {
- ("IPv6", 1, 128)
- } else {
- ("IPv4", 1, 32)
- };
-
- if !(mask >= min && mask <= max) {
- bail!(
- "{} mask '{}' is out of range ({}..{}).",
- ver,
- mask,
- min,
- max
- );
- }
-
- Ok(())
-}
-
-// parse ip address with optional cidr mask
-pub fn parse_address_or_cidr(cidr: &str) -> Result<(String, Option<u8>, bool), Error> {
- // NOTE: This is NOT the same regex as in proxmox-schema as this one has capture groups for
- // the addresses vs cidr portions!
- pub static CIDR_V4_REGEX: LazyLock<Regex> =
- LazyLock::new(|| Regex::new(concatcp!(r"^(", IPV4RE_STR, r")(?:/(\d{1,2}))?$")).unwrap());
- pub static CIDR_V6_REGEX: LazyLock<Regex> =
- LazyLock::new(|| Regex::new(concatcp!(r"^(", IPV6RE_STR, r")(?:/(\d{1,3}))?$")).unwrap());
-
- if let Some(caps) = CIDR_V4_REGEX.captures(cidr) {
- let address = &caps[1];
- if let Some(mask) = caps.get(2) {
- let mask: u8 = mask.as_str().parse()?;
- check_netmask(mask, false)?;
- Ok((address.to_string(), Some(mask), false))
- } else {
- Ok((address.to_string(), None, false))
- }
- } else if let Some(caps) = CIDR_V6_REGEX.captures(cidr) {
- let address = &caps[1];
- if let Some(mask) = caps.get(2) {
- let mask: u8 = mask.as_str().parse()?;
- check_netmask(mask, true)?;
- Ok((address.to_string(), Some(mask), true))
- } else {
- Ok((address.to_string(), None, true))
- }
- } else {
- bail!("invalid address/mask '{}'", cidr);
- }
-}
-
-pub fn get_network_interfaces() -> Result<HashMap<String, bool>, Error> {
- const PROC_NET_DEV: &str = "/proc/net/dev";
-
- #[repr(C)]
- pub struct ifreq {
- ifr_name: [libc::c_uchar; libc::IFNAMSIZ],
- ifru_flags: libc::c_short,
- }
-
- ioctl_read_bad!(get_interface_flags, libc::SIOCGIFFLAGS, ifreq);
-
- static IFACE_LINE_REGEX: LazyLock<Regex> =
- LazyLock::new(|| Regex::new(r"^\s*([^:\s]+):").unwrap());
-
- let raw = std::fs::read_to_string(PROC_NET_DEV)
- .map_err(|err| format_err!("unable to read {} - {}", PROC_NET_DEV, err))?;
-
- let lines = raw.lines();
-
- let sock = socket(
- AddressFamily::Inet,
- SockType::Datagram,
- SockFlag::empty(),
- None,
- )
- .or_else(|_| {
- socket(
- AddressFamily::Inet6,
- SockType::Datagram,
- SockFlag::empty(),
- None,
- )
- })?;
-
- let mut interface_list = HashMap::new();
-
- for line in lines {
- if let Some(cap) = IFACE_LINE_REGEX.captures(line) {
- let ifname = &cap[1];
-
- let mut req = ifreq {
- ifr_name: *b"0000000000000000",
- ifru_flags: 0,
- };
- for (i, b) in std::ffi::CString::new(ifname)?
- .as_bytes_with_nul()
- .iter()
- .enumerate()
- {
- if i < (libc::IFNAMSIZ - 1) {
- req.ifr_name[i] = *b as libc::c_uchar;
- }
- }
- let res = unsafe { get_interface_flags(sock.as_raw_fd(), &mut req)? };
- if res != 0 {
- bail!(
- "ioctl get_interface_flags for '{}' failed ({})",
- ifname,
- res
- );
- }
- let is_up = (req.ifru_flags & (libc::IFF_UP as libc::c_short)) != 0;
- interface_list.insert(ifname.to_string(), is_up);
- }
- }
-
- Ok(interface_list)
-}
-
-pub fn compute_file_diff(filename: &str, shadow: &str) -> Result<String, Error> {
- let output = Command::new("diff")
- .arg("-b")
- .arg("-u")
- .arg(filename)
- .arg(shadow)
- .output()
- .map_err(|err| format_err!("failed to execute diff - {}", err))?;
-
- let diff = proxmox_sys::command::command_output_as_string(output, Some(|c| c == 0 || c == 1))
- .map_err(|err| format_err!("diff failed: {}", err))?;
-
- Ok(diff)
-}
-
-pub fn assert_ifupdown2_installed() -> Result<(), Error> {
- if !Path::new("/usr/share/ifupdown2").exists() {
- bail!("ifupdown2 is not installed.");
- }
-
- Ok(())
-}
-
-pub fn network_reload() -> Result<(), Error> {
- let output = Command::new("ifreload")
- .arg("-a")
- .output()
- .map_err(|err| format_err!("failed to execute 'ifreload' - {}", err))?;
-
- proxmox_sys::command::command_output(output, None)
- .map_err(|err| format_err!("ifreload failed: {}", err))?;
-
- Ok(())
-}
diff --git a/pbs-config/src/network/lexer.rs b/pbs-config/src/network/lexer.rs
deleted file mode 100644
index 6a20f009..00000000
--- a/pbs-config/src/network/lexer.rs
+++ /dev/null
@@ -1,136 +0,0 @@
-use std::collections::{HashMap, VecDeque};
-use std::io::BufRead;
-use std::iter::Iterator;
-use std::sync::LazyLock;
-
-#[derive(Debug, Copy, Clone, Eq, PartialEq)]
-pub enum Token {
- Text,
- Comment,
- DHCP,
- Newline,
- Address,
- Auto,
- Gateway,
- Inet,
- Inet6,
- Iface,
- Loopback,
- Manual,
- Netmask,
- Static,
- Attribute,
- MTU,
- BridgePorts,
- BridgeVlanAware,
- VlanId,
- VlanRawDevice,
- BondSlaves,
- BondMode,
- BondPrimary,
- BondXmitHashPolicy,
- EOF,
-}
-
-static KEYWORDS: LazyLock<HashMap<&'static str, Token>> = LazyLock::new(|| {
- let mut map = HashMap::new();
- map.insert("address", Token::Address);
- map.insert("auto", Token::Auto);
- map.insert("dhcp", Token::DHCP);
- map.insert("gateway", Token::Gateway);
- map.insert("inet", Token::Inet);
- map.insert("inet6", Token::Inet6);
- map.insert("iface", Token::Iface);
- map.insert("loopback", Token::Loopback);
- map.insert("manual", Token::Manual);
- map.insert("netmask", Token::Netmask);
- map.insert("static", Token::Static);
- map.insert("mtu", Token::MTU);
- map.insert("bridge-ports", Token::BridgePorts);
- map.insert("bridge_ports", Token::BridgePorts);
- map.insert("bridge-vlan-aware", Token::BridgeVlanAware);
- map.insert("bridge_vlan_aware", Token::BridgeVlanAware);
- map.insert("vlan-id", Token::VlanId);
- map.insert("vlan_id", Token::VlanId);
- map.insert("vlan-raw-device", Token::VlanRawDevice);
- map.insert("vlan_raw_device", Token::VlanRawDevice);
- map.insert("bond-slaves", Token::BondSlaves);
- map.insert("bond_slaves", Token::BondSlaves);
- map.insert("bond-mode", Token::BondMode);
- map.insert("bond-primary", Token::BondPrimary);
- map.insert("bond_primary", Token::BondPrimary);
- map.insert("bond_xmit_hash_policy", Token::BondXmitHashPolicy);
- map.insert("bond-xmit-hash-policy", Token::BondXmitHashPolicy);
- map
-});
-
-pub struct Lexer<R> {
- input: R,
- eof_count: usize,
- cur_line: Option<VecDeque<(Token, String)>>,
-}
-
-impl<R: BufRead> Lexer<R> {
- pub fn new(input: R) -> Self {
- Self {
- input,
- eof_count: 0,
- cur_line: None,
- }
- }
-
- fn split_line(line: &str) -> VecDeque<(Token, String)> {
- if let Some(comment) = line.strip_prefix('#') {
- let mut res = VecDeque::new();
- res.push_back((Token::Comment, comment.trim().to_string()));
- return res;
- }
- let mut list: VecDeque<(Token, String)> = line
- .split_ascii_whitespace()
- .map(|text| {
- let token = KEYWORDS.get(text).unwrap_or(&Token::Text);
- (*token, text.to_string())
- })
- .collect();
-
- if line.starts_with(|c: char| c.is_ascii_whitespace() && c != '\n') {
- list.push_front((Token::Attribute, String::from("\t")));
- }
- list
- }
-}
-
-impl<R: BufRead> Iterator for Lexer<R> {
- type Item = Result<(Token, String), std::io::Error>;
-
- fn next(&mut self) -> Option<Self::Item> {
- if self.cur_line.is_none() {
- let mut line = String::new();
- match self.input.read_line(&mut line) {
- Err(err) => return Some(Err(err)),
- Ok(0) => {
- self.eof_count += 1;
- if self.eof_count == 1 {
- return Some(Ok((Token::EOF, String::new())));
- }
- return None;
- }
- _ => {}
- }
- self.cur_line = Some(Self::split_line(&line));
- }
-
- match self.cur_line {
- Some(ref mut cur_line) => {
- if cur_line.is_empty() {
- self.cur_line = None;
- Some(Ok((Token::Newline, String::from("\n"))))
- } else {
- let (token, text) = cur_line.pop_front().unwrap();
- Some(Ok((token, text)))
- }
- }
- None => None,
- }
- }
-}
diff --git a/pbs-config/src/network/mod.rs b/pbs-config/src/network/mod.rs
deleted file mode 100644
index 21ad9943..00000000
--- a/pbs-config/src/network/mod.rs
+++ /dev/null
@@ -1,687 +0,0 @@
-use std::collections::{BTreeMap, HashMap, HashSet};
-use std::io::Write;
-use std::sync::LazyLock;
-
-use anyhow::{bail, format_err, Error};
-use regex::Regex;
-use serde::de::{value, Deserialize, IntoDeserializer};
-
-use proxmox_sys::{fs::replace_file, fs::CreateOptions};
-
-mod helper;
-pub use helper::*;
-
-mod lexer;
-pub use lexer::*;
-
-mod parser;
-pub use parser::*;
-
-use pbs_api_types::{
- BondXmitHashPolicy, Interface, LinuxBondMode, NetworkConfigMethod, NetworkInterfaceType,
-};
-
-use crate::{open_backup_lockfile, BackupLockGuard};
-
-static PHYSICAL_NIC_REGEX: LazyLock<Regex> =
- LazyLock::new(|| Regex::new(r"^(?:eth\d+|en[^:.]+|ib\d+)$").unwrap());
-static VLAN_INTERFACE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
- Regex::new(r"^(?P<vlan_raw_device>\S+)\.(?P<vlan_id>\d+)|vlan(?P<vlan_id2>\d+)$").unwrap()
-});
-
-pub fn is_physical_nic(iface: &str) -> bool {
- PHYSICAL_NIC_REGEX.is_match(iface)
-}
-
-pub fn bond_mode_from_str(s: &str) -> Result<LinuxBondMode, Error> {
- LinuxBondMode::deserialize(s.into_deserializer())
- .map_err(|_: value::Error| format_err!("invalid bond_mode '{}'", s))
-}
-
-pub fn bond_xmit_hash_policy_from_str(s: &str) -> Result<BondXmitHashPolicy, Error> {
- BondXmitHashPolicy::deserialize(s.into_deserializer())
- .map_err(|_: value::Error| format_err!("invalid bond_xmit_hash_policy '{}'", s))
-}
-
-pub fn parse_vlan_id_from_name(iface_name: &str) -> Option<u16> {
- VLAN_INTERFACE_REGEX.captures(iface_name).and_then(|cap| {
- cap.name("vlan_id")
- .or(cap.name("vlan_id2"))
- .and_then(|id| id.as_str().parse::<u16>().ok())
- })
-}
-
-pub fn parse_vlan_raw_device_from_name(iface_name: &str) -> Option<&str> {
- VLAN_INTERFACE_REGEX
- .captures(iface_name)
- .and_then(|cap| cap.name("vlan_raw_device"))
- .map(Into::into)
-}
-
-// Write attributes not depending on address family
-fn write_iface_attributes(iface: &Interface, w: &mut dyn Write) -> Result<(), Error> {
- static EMPTY_LIST: Vec<String> = Vec::new();
-
- match iface.interface_type {
- NetworkInterfaceType::Bridge => {
- if let Some(true) = iface.bridge_vlan_aware {
- writeln!(w, "\tbridge-vlan-aware yes")?;
- }
- let ports = iface.bridge_ports.as_ref().unwrap_or(&EMPTY_LIST);
- if ports.is_empty() {
- writeln!(w, "\tbridge-ports none")?;
- } else {
- writeln!(w, "\tbridge-ports {}", ports.join(" "))?;
- }
- }
- NetworkInterfaceType::Bond => {
- let mode = iface.bond_mode.unwrap_or(LinuxBondMode::BalanceRr);
- writeln!(w, "\tbond-mode {mode}")?;
- if let Some(primary) = &iface.bond_primary {
- if mode == LinuxBondMode::ActiveBackup {
- writeln!(w, "\tbond-primary {}", primary)?;
- }
- }
-
- if let Some(xmit_policy) = &iface.bond_xmit_hash_policy {
- if mode == LinuxBondMode::Ieee802_3ad || mode == LinuxBondMode::BalanceXor {
- writeln!(w, "\tbond_xmit_hash_policy {xmit_policy}")?;
- }
- }
-
- let slaves = iface.slaves.as_ref().unwrap_or(&EMPTY_LIST);
- if slaves.is_empty() {
- writeln!(w, "\tbond-slaves none")?;
- } else {
- writeln!(w, "\tbond-slaves {}", slaves.join(" "))?;
- }
- }
- NetworkInterfaceType::Vlan => {
- if let Some(vlan_id) = iface.vlan_id {
- writeln!(w, "\tvlan-id {vlan_id}")?;
- }
- if let Some(vlan_raw_device) = &iface.vlan_raw_device {
- writeln!(w, "\tvlan-raw-device {vlan_raw_device}")?;
- }
- }
- _ => {}
- }
-
- if let Some(mtu) = iface.mtu {
- writeln!(w, "\tmtu {}", mtu)?;
- }
-
- Ok(())
-}
-
-// Write attributes depending on address family inet (IPv4)
-fn write_iface_attributes_v4(
- iface: &Interface,
- w: &mut dyn Write,
- method: NetworkConfigMethod,
-) -> Result<(), Error> {
- if method == NetworkConfigMethod::Static {
- if let Some(address) = &iface.cidr {
- writeln!(w, "\taddress {}", address)?;
- }
- if let Some(gateway) = &iface.gateway {
- writeln!(w, "\tgateway {}", gateway)?;
- }
- }
-
- for option in &iface.options {
- writeln!(w, "\t{}", option)?;
- }
-
- if let Some(ref comments) = iface.comments {
- for comment in comments.lines() {
- writeln!(w, "#{}", comment)?;
- }
- }
-
- Ok(())
-}
-
-/// Write attributes depending on address family inet6 (IPv6)
-fn write_iface_attributes_v6(
- iface: &Interface,
- w: &mut dyn Write,
- method: NetworkConfigMethod,
-) -> Result<(), Error> {
- if method == NetworkConfigMethod::Static {
- if let Some(address) = &iface.cidr6 {
- writeln!(w, "\taddress {}", address)?;
- }
- if let Some(gateway) = &iface.gateway6 {
- writeln!(w, "\tgateway {}", gateway)?;
- }
- }
-
- for option in &iface.options6 {
- writeln!(w, "\t{}", option)?;
- }
-
- if let Some(ref comments) = iface.comments6 {
- for comment in comments.lines() {
- writeln!(w, "#{}", comment)?;
- }
- }
-
- Ok(())
-}
-
-fn write_iface(iface: &Interface, w: &mut dyn Write) -> Result<(), Error> {
- fn method_to_str(method: NetworkConfigMethod) -> &'static str {
- match method {
- NetworkConfigMethod::Static => "static",
- NetworkConfigMethod::Loopback => "loopback",
- NetworkConfigMethod::Manual => "manual",
- NetworkConfigMethod::DHCP => "dhcp",
- }
- }
-
- if iface.method.is_none() && iface.method6.is_none() {
- return Ok(());
- }
-
- if iface.autostart {
- writeln!(w, "auto {}", iface.name)?;
- }
-
- if let Some(method) = iface.method {
- writeln!(w, "iface {} inet {}", iface.name, method_to_str(method))?;
- write_iface_attributes_v4(iface, w, method)?;
- write_iface_attributes(iface, w)?;
- writeln!(w)?;
- }
-
- if let Some(method6) = iface.method6 {
- let mut skip_v6 = false; // avoid empty inet6 manual entry
- if iface.method.is_some()
- && method6 == NetworkConfigMethod::Manual
- && iface.comments6.is_none()
- && iface.options6.is_empty()
- {
- skip_v6 = true;
- }
-
- if !skip_v6 {
- writeln!(w, "iface {} inet6 {}", iface.name, method_to_str(method6))?;
- write_iface_attributes_v6(iface, w, method6)?;
- if iface.method.is_none() {
- // only write common attributes once
- write_iface_attributes(iface, w)?;
- }
- writeln!(w)?;
- }
- }
-
- Ok(())
-}
-
-#[derive(Debug)]
-enum NetworkOrderEntry {
- Iface(String),
- Comment(String),
- Option(String),
-}
-
-#[derive(Debug, Default)]
-pub struct NetworkConfig {
- pub interfaces: BTreeMap<String, Interface>,
- order: Vec<NetworkOrderEntry>,
-}
-
-impl TryFrom<NetworkConfig> for String {
- type Error = Error;
-
- fn try_from(config: NetworkConfig) -> Result<Self, Self::Error> {
- let mut output = Vec::new();
- config.write_config(&mut output)?;
- let res = String::from_utf8(output)?;
- Ok(res)
- }
-}
-
-impl NetworkConfig {
- pub fn new() -> Self {
- Self {
- interfaces: BTreeMap::new(),
- order: Vec::new(),
- }
- }
-
- pub fn lookup(&self, name: &str) -> Result<&Interface, Error> {
- let interface = self
- .interfaces
- .get(name)
- .ok_or_else(|| format_err!("interface '{}' does not exist.", name))?;
- Ok(interface)
- }
-
- pub fn lookup_mut(&mut self, name: &str) -> Result<&mut Interface, Error> {
- let interface = self
- .interfaces
- .get_mut(name)
- .ok_or_else(|| format_err!("interface '{}' does not exist.", name))?;
- Ok(interface)
- }
-
- /// Check if ports are used only once
- fn check_port_usage(&self) -> Result<(), Error> {
- let mut used_ports = HashMap::new();
- let mut check_port_usage = |iface, ports: &Vec<String>| {
- for port in ports.iter() {
- if let Some(prev_iface) = used_ports.get(port) {
- bail!(
- "iface '{}' port '{}' is already used on interface '{}'",
- iface,
- port,
- prev_iface
- );
- }
- used_ports.insert(port.to_string(), iface);
- }
- Ok(())
- };
-
- for (iface, interface) in self.interfaces.iter() {
- if let Some(ports) = &interface.bridge_ports {
- check_port_usage(iface, ports)?;
- }
- if let Some(slaves) = &interface.slaves {
- check_port_usage(iface, slaves)?;
- }
- }
- Ok(())
- }
-
- /// Check if child mtu is less or equal than parent mtu
- fn check_mtu(&self, parent_name: &str, child_name: &str) -> Result<(), Error> {
- let parent = self
- .interfaces
- .get(parent_name)
- .ok_or_else(|| format_err!("check_mtu - missing parent interface '{}'", parent_name))?;
- let child = self
- .interfaces
- .get(child_name)
- .ok_or_else(|| format_err!("check_mtu - missing child interface '{}'", child_name))?;
-
- let child_mtu = match child.mtu {
- Some(mtu) => mtu,
- None => return Ok(()),
- };
-
- let parent_mtu = match parent.mtu {
- Some(mtu) => mtu,
- None => {
- if parent.interface_type == NetworkInterfaceType::Bond {
- child_mtu
- } else {
- 1500
- }
- }
- };
-
- if parent_mtu < child_mtu {
- bail!(
- "interface '{}' - mtu {} is lower than '{}' - mtu {}\n",
- parent_name,
- parent_mtu,
- child_name,
- child_mtu
- );
- }
-
- Ok(())
- }
-
- /// Check if bond slaves exists
- fn check_bond_slaves(&self) -> Result<(), Error> {
- for (iface, interface) in self.interfaces.iter() {
- if let Some(slaves) = &interface.slaves {
- for slave in slaves.iter() {
- match self.interfaces.get(slave) {
- Some(entry) => {
- if entry.interface_type != NetworkInterfaceType::Eth {
- bail!(
- "bond '{}' - wrong interface type on slave '{}' ({:?} != {:?})",
- iface,
- slave,
- entry.interface_type,
- NetworkInterfaceType::Eth
- );
- }
- }
- None => {
- bail!("bond '{}' - unable to find slave '{}'", iface, slave);
- }
- }
- self.check_mtu(iface, slave)?;
- }
- }
- }
- Ok(())
- }
-
- /// Check if bridge ports exists
- fn check_bridge_ports(&self) -> Result<(), Error> {
- static VLAN_INTERFACE_REGEX: LazyLock<Regex> =
- LazyLock::new(|| Regex::new(r"^(\S+)\.(\d+)$").unwrap());
-
- for (iface, interface) in self.interfaces.iter() {
- if let Some(ports) = &interface.bridge_ports {
- for port in ports.iter() {
- let captures = VLAN_INTERFACE_REGEX.captures(port);
- let port = if let Some(ref caps) = captures {
- &caps[1]
- } else {
- port.as_str()
- };
- if !self.interfaces.contains_key(port) {
- bail!("bridge '{}' - unable to find port '{}'", iface, port);
- }
- self.check_mtu(iface, port)?;
- }
- }
- }
- Ok(())
- }
-
- fn write_config(&self, w: &mut dyn Write) -> Result<(), Error> {
- self.check_port_usage()?;
- self.check_bond_slaves()?;
- self.check_bridge_ports()?;
-
- let mut done = HashSet::new();
-
- let mut last_entry_was_comment = false;
-
- for entry in self.order.iter() {
- match entry {
- NetworkOrderEntry::Comment(comment) => {
- writeln!(w, "#{}", comment)?;
- last_entry_was_comment = true;
- }
- NetworkOrderEntry::Option(option) => {
- if last_entry_was_comment {
- writeln!(w)?;
- }
- last_entry_was_comment = false;
- writeln!(w, "{}", option)?;
- writeln!(w)?;
- }
- NetworkOrderEntry::Iface(name) => {
- let interface = match self.interfaces.get(name) {
- Some(interface) => interface,
- None => continue,
- };
-
- if last_entry_was_comment {
- writeln!(w)?;
- }
- last_entry_was_comment = false;
-
- if done.contains(name) {
- continue;
- }
- done.insert(name);
-
- write_iface(interface, w)?;
- }
- }
- }
-
- for (name, interface) in &self.interfaces {
- if done.contains(name) {
- continue;
- }
- write_iface(interface, w)?;
- }
- Ok(())
- }
-}
-
-pub const NETWORK_INTERFACES_FILENAME: &str = "/etc/network/interfaces";
-pub const NETWORK_INTERFACES_NEW_FILENAME: &str = "/etc/network/interfaces.new";
-pub const NETWORK_LOCKFILE: &str = "/var/lock/pve-network.lck";
-
-pub fn lock_config() -> Result<BackupLockGuard, Error> {
- open_backup_lockfile(NETWORK_LOCKFILE, None, true)
-}
-
-pub fn config() -> Result<(NetworkConfig, [u8; 32]), Error> {
- let content =
- match proxmox_sys::fs::file_get_optional_contents(NETWORK_INTERFACES_NEW_FILENAME)? {
- Some(content) => content,
- None => {
- let content =
- proxmox_sys::fs::file_get_optional_contents(NETWORK_INTERFACES_FILENAME)?;
- content.unwrap_or_default()
- }
- };
-
- let digest = openssl::sha::sha256(&content);
-
- let existing_interfaces = get_network_interfaces()?;
- let mut parser = NetworkParser::new(&content[..]);
- let data = parser.parse_interfaces(Some(&existing_interfaces))?;
-
- Ok((data, digest))
-}
-
-pub fn changes() -> Result<String, Error> {
- if !std::path::Path::new(NETWORK_INTERFACES_NEW_FILENAME).exists() {
- return Ok(String::new());
- }
-
- compute_file_diff(NETWORK_INTERFACES_FILENAME, NETWORK_INTERFACES_NEW_FILENAME)
-}
-
-pub fn save_config(config: &NetworkConfig) -> Result<(), Error> {
- let mut raw = Vec::new();
- config.write_config(&mut raw)?;
-
- let mode = nix::sys::stat::Mode::from_bits_truncate(0o0644);
- // set the correct owner/group/permissions while saving file
- // owner(rw) = root, group(r)=root, others(r)
- let options = CreateOptions::new()
- .perm(mode)
- .owner(nix::unistd::ROOT)
- .group(nix::unistd::Gid::from_raw(0));
-
- replace_file(NETWORK_INTERFACES_NEW_FILENAME, &raw, options, true)?;
-
- Ok(())
-}
-
-// shell completion helper
-pub fn complete_interface_name(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
- match config() {
- Ok((data, _digest)) => data.interfaces.keys().map(|id| id.to_string()).collect(),
- Err(_) => Vec::new(),
- }
-}
-
-pub fn complete_port_list(arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
- let mut ports = Vec::new();
- match config() {
- Ok((data, _digest)) => {
- for (iface, interface) in data.interfaces.iter() {
- if interface.interface_type == NetworkInterfaceType::Eth {
- ports.push(iface.to_string());
- }
- }
- }
- Err(_) => return Vec::new(),
- };
-
- let arg = arg.trim();
- let prefix = if let Some(idx) = arg.rfind(',') {
- &arg[..idx + 1]
- } else {
- ""
- };
- ports
- .iter()
- .map(|port| format!("{}{}", prefix, port))
- .collect()
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- use NetworkConfigMethod::*;
- use NetworkInterfaceType::*;
- use NetworkOrderEntry::*;
-
- #[test]
- fn test_write_network_config_manual() {
- let iface_name = String::from("enp3s0");
- let mut iface = Interface::new(iface_name.clone());
- iface.interface_type = Eth;
- iface.method = Some(Manual);
- iface.active = true;
-
- let nw_config = NetworkConfig {
- interfaces: BTreeMap::from([(iface_name.clone(), iface)]),
- order: vec![Iface(iface_name.clone())],
- };
-
- assert_eq!(
- String::try_from(nw_config).unwrap().trim(),
- r#"iface enp3s0 inet manual"#
- );
- }
-
- #[test]
- fn test_write_network_config_static() {
- let iface_name = String::from("enp3s0");
- let mut iface = Interface::new(iface_name.clone());
- iface.interface_type = Eth;
- iface.method = Some(Static);
- iface.cidr = Some(String::from("10.0.0.100/16"));
- iface.active = true;
-
- let nw_config = NetworkConfig {
- interfaces: BTreeMap::from([(iface_name.clone(), iface)]),
- order: vec![Iface(iface_name.clone())],
- };
- assert_eq!(
- String::try_from(nw_config).unwrap().trim(),
- r#"
-iface enp3s0 inet static
- address 10.0.0.100/16"#
- .to_string()
- .trim()
- );
- }
-
- #[test]
- fn test_write_network_config_static_with_gateway() {
- let iface_name = String::from("enp3s0");
- let mut iface = Interface::new(iface_name.clone());
- iface.interface_type = Eth;
- iface.method = Some(Static);
- iface.cidr = Some(String::from("10.0.0.100/16"));
- iface.gateway = Some(String::from("10.0.0.1"));
- iface.active = true;
-
- let nw_config = NetworkConfig {
- interfaces: BTreeMap::from([(iface_name.clone(), iface)]),
- order: vec![Iface(iface_name.clone())],
- };
- assert_eq!(
- String::try_from(nw_config).unwrap().trim(),
- r#"
-iface enp3s0 inet static
- address 10.0.0.100/16
- gateway 10.0.0.1"#
- .to_string()
- .trim()
- );
- }
-
- #[test]
- fn test_write_network_config_vlan_id_in_name() {
- let iface_name = String::from("vmbr0.100");
- let mut iface = Interface::new(iface_name.clone());
- iface.interface_type = Vlan;
- iface.method = Some(Manual);
- iface.active = true;
-
- let nw_config = NetworkConfig {
- interfaces: BTreeMap::from([(iface_name.clone(), iface)]),
- order: vec![Iface(iface_name.clone())],
- };
- assert_eq!(
- String::try_from(nw_config).unwrap().trim(),
- "iface vmbr0.100 inet manual"
- );
- }
-
- #[test]
- fn test_write_network_config_vlan_with_raw_device() {
- let iface_name = String::from("vlan100");
- let mut iface = Interface::new(iface_name.clone());
- iface.interface_type = Vlan;
- iface.vlan_raw_device = Some(String::from("vmbr0"));
- iface.method = Some(Manual);
- iface.active = true;
-
- let nw_config = NetworkConfig {
- interfaces: BTreeMap::from([(iface_name.clone(), iface)]),
- order: vec![Iface(iface_name.clone())],
- };
- assert_eq!(
- String::try_from(nw_config).unwrap().trim(),
- r#"
-iface vlan100 inet manual
- vlan-raw-device vmbr0"#
- .trim()
- );
- }
-
- #[test]
- fn test_write_network_config_vlan_with_individual_name() {
- let iface_name = String::from("individual_name");
- let mut iface = Interface::new(iface_name.clone());
- iface.interface_type = Vlan;
- iface.vlan_raw_device = Some(String::from("vmbr0"));
- iface.vlan_id = Some(100);
- iface.method = Some(Manual);
- iface.active = true;
-
- let nw_config = NetworkConfig {
- interfaces: BTreeMap::from([(iface_name.clone(), iface)]),
- order: vec![Iface(iface_name.clone())],
- };
- assert_eq!(
- String::try_from(nw_config).unwrap().trim(),
- r#"
-iface individual_name inet manual
- vlan-id 100
- vlan-raw-device vmbr0"#
- .trim()
- );
- }
-
- #[test]
- fn test_vlan_parse_vlan_id_from_name() {
- assert_eq!(parse_vlan_id_from_name("vlan100"), Some(100));
- assert_eq!(parse_vlan_id_from_name("vlan"), None);
- assert_eq!(parse_vlan_id_from_name("arbitrary"), None);
- assert_eq!(parse_vlan_id_from_name("vmbr0.100"), Some(100));
- assert_eq!(parse_vlan_id_from_name("vmbr0"), None);
- // assert_eq!(parse_vlan_id_from_name("vmbr0.1.400"), Some(400)); // NOTE ifupdown2 does actually support this
- }
-
- #[test]
- fn test_vlan_parse_vlan_raw_device_from_name() {
- assert_eq!(parse_vlan_raw_device_from_name("vlan100"), None);
- assert_eq!(parse_vlan_raw_device_from_name("arbitrary"), None);
- assert_eq!(parse_vlan_raw_device_from_name("vmbr0"), None);
- assert_eq!(parse_vlan_raw_device_from_name("vmbr0.200"), Some("vmbr0"));
- }
-}
diff --git a/pbs-config/src/network/parser.rs b/pbs-config/src/network/parser.rs
deleted file mode 100644
index a5d05c6e..00000000
--- a/pbs-config/src/network/parser.rs
+++ /dev/null
@@ -1,846 +0,0 @@
-use crate::network::VLAN_INTERFACE_REGEX;
-
-use std::collections::{HashMap, HashSet};
-use std::io::BufRead;
-use std::iter::{Iterator, Peekable};
-use std::sync::LazyLock;
-
-use anyhow::{bail, format_err, Error};
-use regex::Regex;
-
-use super::helper::*;
-use super::lexer::*;
-
-use super::{
- bond_mode_from_str, bond_xmit_hash_policy_from_str, Interface, NetworkConfig,
- NetworkConfigMethod, NetworkInterfaceType, NetworkOrderEntry,
-};
-
-fn set_method_v4(iface: &mut Interface, method: NetworkConfigMethod) -> Result<(), Error> {
- if iface.method.is_none() {
- iface.method = Some(method);
- } else {
- bail!("inet configuration method already set.");
- }
- Ok(())
-}
-
-fn set_method_v6(iface: &mut Interface, method: NetworkConfigMethod) -> Result<(), Error> {
- if iface.method6.is_none() {
- iface.method6 = Some(method);
- } else {
- bail!("inet6 configuration method already set.");
- }
- Ok(())
-}
-
-fn set_cidr_v4(iface: &mut Interface, address: String) -> Result<(), Error> {
- if iface.cidr.is_none() {
- iface.cidr = Some(address);
- } else {
- bail!("duplicate IPv4 address.");
- }
- Ok(())
-}
-
-fn set_gateway_v4(iface: &mut Interface, gateway: String) -> Result<(), Error> {
- if iface.gateway.is_none() {
- iface.gateway = Some(gateway);
- } else {
- bail!("duplicate IPv4 gateway.");
- }
- Ok(())
-}
-
-fn set_cidr_v6(iface: &mut Interface, address: String) -> Result<(), Error> {
- if iface.cidr6.is_none() {
- iface.cidr6 = Some(address);
- } else {
- bail!("duplicate IPv6 address.");
- }
- Ok(())
-}
-
-fn set_gateway_v6(iface: &mut Interface, gateway: String) -> Result<(), Error> {
- if iface.gateway6.is_none() {
- iface.gateway6 = Some(gateway);
- } else {
- bail!("duplicate IPv4 gateway.");
- }
- Ok(())
-}
-
-fn set_interface_type(
- iface: &mut Interface,
- interface_type: NetworkInterfaceType,
-) -> Result<(), Error> {
- if iface.interface_type == NetworkInterfaceType::Unknown {
- iface.interface_type = interface_type;
- } else if iface.interface_type != interface_type {
- bail!(
- "interface type already defined - cannot change from {:?} to {:?}",
- iface.interface_type,
- interface_type
- );
- }
- Ok(())
-}
-
-pub struct NetworkParser<R: BufRead> {
- input: Peekable<Lexer<R>>,
- line_nr: usize,
-}
-
-impl<R: BufRead> NetworkParser<R> {
- pub fn new(reader: R) -> Self {
- let input = Lexer::new(reader).peekable();
- Self { input, line_nr: 1 }
- }
-
- fn peek(&mut self) -> Result<Token, Error> {
- match self.input.peek() {
- Some(Err(err)) => {
- bail!("input error - {}", err);
- }
- Some(Ok((token, _))) => Ok(*token),
- None => {
- bail!("got unexpected end of stream (inside peek)");
- }
- }
- }
-
- fn next(&mut self) -> Result<(Token, String), Error> {
- match self.input.next() {
- Some(Err(err)) => {
- bail!("input error - {}", err);
- }
- Some(Ok((token, text))) => {
- if token == Token::Newline {
- self.line_nr += 1;
- }
- Ok((token, text))
- }
- None => {
- bail!("got unexpected end of stream (inside peek)");
- }
- }
- }
-
- fn next_text(&mut self) -> Result<String, Error> {
- match self.next()? {
- (Token::Text, text) => Ok(text),
- (unexpected, _) => bail!("got unexpected token {:?} (expecting Text)", unexpected),
- }
- }
-
- fn eat(&mut self, expected: Token) -> Result<String, Error> {
- let (next, text) = self.next()?;
- if next != expected {
- bail!("expected {:?}, got {:?}", expected, next);
- }
- Ok(text)
- }
-
- fn parse_auto(&mut self, auto_flag: &mut HashSet<String>) -> Result<(), Error> {
- self.eat(Token::Auto)?;
-
- loop {
- match self.next()? {
- (Token::Text, iface) => {
- auto_flag.insert(iface.to_string());
- }
- (Token::Newline, _) => break,
- unexpected => {
- bail!("expected {:?}, got {:?}", Token::Text, unexpected);
- }
- }
- }
-
- Ok(())
- }
-
- fn parse_netmask(&mut self) -> Result<u8, Error> {
- self.eat(Token::Netmask)?;
- let netmask = self.next_text()?;
-
- let mask = if let Some(mask) = IPV4_MASK_HASH_LOCALNET.get(netmask.as_str()) {
- *mask
- } else {
- match netmask.as_str().parse::<u8>() {
- Ok(mask) => mask,
- Err(err) => {
- bail!("unable to parse netmask '{}' - {}", netmask, err);
- }
- }
- };
-
- self.eat(Token::Newline)?;
-
- Ok(mask)
- }
-
- fn parse_iface_address(&mut self) -> Result<(String, Option<u8>, bool), Error> {
- self.eat(Token::Address)?;
- let cidr = self.next_text()?;
-
- let (_address, mask, ipv6) = parse_address_or_cidr(&cidr)?;
-
- self.eat(Token::Newline)?;
-
- Ok((cidr, mask, ipv6))
- }
-
- fn parse_iface_gateway(&mut self, interface: &mut Interface) -> Result<(), Error> {
- self.eat(Token::Gateway)?;
- let gateway = self.next_text()?;
-
- if pbs_api_types::IP_REGEX.is_match(&gateway) {
- if gateway.contains(':') {
- set_gateway_v6(interface, gateway)?;
- } else {
- set_gateway_v4(interface, gateway)?;
- }
- } else {
- bail!("unable to parse gateway address");
- }
-
- self.eat(Token::Newline)?;
-
- Ok(())
- }
-
- fn parse_iface_mtu(&mut self) -> Result<u64, Error> {
- self.eat(Token::MTU)?;
-
- let mtu = self.next_text()?;
- let mtu = match mtu.parse::<u64>() {
- Ok(mtu) => mtu,
- Err(err) => {
- bail!("unable to parse mtu value '{}' - {}", mtu, err);
- }
- };
-
- self.eat(Token::Newline)?;
-
- Ok(mtu)
- }
-
- fn parse_yes_no(&mut self) -> Result<bool, Error> {
- let text = self.next_text()?;
- let value = match text.to_lowercase().as_str() {
- "yes" => true,
- "no" => false,
- _ => {
- bail!("unable to bool value '{}' - (expected yes/no)", text);
- }
- };
-
- self.eat(Token::Newline)?;
-
- Ok(value)
- }
-
- fn parse_to_eol(&mut self) -> Result<String, Error> {
- let mut line = String::new();
- loop {
- match self.next()? {
- (Token::Newline, _) => return Ok(line),
- (_, text) => {
- if !line.is_empty() {
- line.push(' ');
- }
- line.push_str(&text);
- }
- }
- }
- }
-
- fn parse_iface_list(&mut self) -> Result<Vec<String>, Error> {
- let mut list = Vec::new();
-
- loop {
- let (token, text) = self.next()?;
- match token {
- Token::Newline => break,
- Token::Text => {
- if &text != "none" {
- list.push(text);
- }
- }
- _ => bail!(
- "unable to parse interface list - unexpected token '{:?}'",
- token
- ),
- }
- }
-
- Ok(list)
- }
-
- fn parse_iface_attributes(
- &mut self,
- interface: &mut Interface,
- address_family_v4: bool,
- address_family_v6: bool,
- ) -> Result<(), Error> {
- let mut netmask = None;
- let mut address_list = Vec::new();
-
- loop {
- match self.peek()? {
- Token::Attribute => {
- self.eat(Token::Attribute)?;
- }
- Token::Comment => {
- let comment = self.eat(Token::Comment)?;
- if !address_family_v4 && address_family_v6 {
- let mut comments = interface.comments6.take().unwrap_or_default();
- if !comments.is_empty() {
- comments.push('\n');
- }
- comments.push_str(&comment);
- interface.comments6 = Some(comments);
- } else {
- let mut comments = interface.comments.take().unwrap_or_default();
- if !comments.is_empty() {
- comments.push('\n');
- }
- comments.push_str(&comment);
- interface.comments = Some(comments);
- }
- self.eat(Token::Newline)?;
- continue;
- }
- _ => break,
- }
-
- match self.peek()? {
- Token::Address => {
- let (cidr, mask, is_v6) = self.parse_iface_address()?;
- address_list.push((cidr, mask, is_v6));
- }
- Token::Gateway => self.parse_iface_gateway(interface)?,
- Token::Netmask => {
- //Note: netmask is deprecated, but we try to do our best
- netmask = Some(self.parse_netmask()?);
- }
- Token::MTU => {
- let mtu = self.parse_iface_mtu()?;
- interface.mtu = Some(mtu);
- }
- Token::BridgeVlanAware => {
- self.eat(Token::BridgeVlanAware)?;
- let bridge_vlan_aware = self.parse_yes_no()?;
- interface.bridge_vlan_aware = Some(bridge_vlan_aware);
- }
- Token::BridgePorts => {
- self.eat(Token::BridgePorts)?;
- let ports = self.parse_iface_list()?;
- interface.bridge_ports = Some(ports);
- set_interface_type(interface, NetworkInterfaceType::Bridge)?;
- }
- Token::BondSlaves => {
- self.eat(Token::BondSlaves)?;
- let slaves = self.parse_iface_list()?;
- interface.slaves = Some(slaves);
- set_interface_type(interface, NetworkInterfaceType::Bond)?;
- }
- Token::BondMode => {
- self.eat(Token::BondMode)?;
- let mode = self.next_text()?;
- interface.bond_mode = Some(bond_mode_from_str(&mode)?);
- self.eat(Token::Newline)?;
- }
- Token::BondPrimary => {
- self.eat(Token::BondPrimary)?;
- let primary = self.next_text()?;
- interface.bond_primary = Some(primary);
- self.eat(Token::Newline)?;
- }
- Token::BondXmitHashPolicy => {
- self.eat(Token::BondXmitHashPolicy)?;
- let policy = bond_xmit_hash_policy_from_str(&self.next_text()?)?;
- interface.bond_xmit_hash_policy = Some(policy);
- self.eat(Token::Newline)?;
- }
- Token::VlanId => {
- self.eat(Token::VlanId)?;
- let vlan_id = self.next_text()?.parse()?;
- interface.vlan_id = Some(vlan_id);
- set_interface_type(interface, NetworkInterfaceType::Vlan)?;
- self.eat(Token::Newline)?;
- }
- Token::VlanRawDevice => {
- self.eat(Token::VlanRawDevice)?;
- let vlan_raw_device = self.next_text()?;
- interface.vlan_raw_device = Some(vlan_raw_device);
- set_interface_type(interface, NetworkInterfaceType::Vlan)?;
- self.eat(Token::Newline)?;
- }
- _ => {
- // parse addon attributes
- let option = self.parse_to_eol()?;
- if !option.is_empty() {
- if !address_family_v4 && address_family_v6 {
- interface.options6.push(option);
- } else {
- interface.options.push(option);
- }
- };
- }
- }
- }
-
- #[allow(clippy::comparison_chain)]
- if let Some(netmask) = netmask {
- if address_list.len() > 1 {
- bail!("unable to apply netmask to multiple addresses (please use cidr notation)");
- } else if address_list.len() == 1 {
- let (mut cidr, mask, is_v6) = address_list.pop().unwrap();
- if mask.is_some() {
- // address already has a mask - ignore netmask
- } else {
- use std::fmt::Write as _;
- check_netmask(netmask, is_v6)?;
- let _ = write!(cidr, "/{}", netmask);
- }
- if is_v6 {
- set_cidr_v6(interface, cidr)?;
- } else {
- set_cidr_v4(interface, cidr)?;
- }
- } else {
- // no address - simply ignore useless netmask
- }
- } else {
- for (cidr, mask, is_v6) in address_list {
- if mask.is_none() {
- bail!("missing netmask in '{}'", cidr);
- }
- if is_v6 {
- set_cidr_v6(interface, cidr)?;
- } else {
- set_cidr_v4(interface, cidr)?;
- }
- }
- }
-
- Ok(())
- }
-
- fn parse_iface(&mut self, config: &mut NetworkConfig) -> Result<(), Error> {
- self.eat(Token::Iface)?;
- let iface = self.next_text()?;
-
- let mut address_family_v4 = false;
- let mut address_family_v6 = false;
- let mut config_method = None;
-
- loop {
- let (token, text) = self.next()?;
- match token {
- Token::Newline => break,
- Token::Inet => address_family_v4 = true,
- Token::Inet6 => address_family_v6 = true,
- Token::Loopback => config_method = Some(NetworkConfigMethod::Loopback),
- Token::Static => config_method = Some(NetworkConfigMethod::Static),
- Token::Manual => config_method = Some(NetworkConfigMethod::Manual),
- Token::DHCP => config_method = Some(NetworkConfigMethod::DHCP),
- _ => bail!("unknown iface option {}", text),
- }
- }
-
- let config_method = config_method.unwrap_or(NetworkConfigMethod::Static);
-
- if !(address_family_v4 || address_family_v6) {
- address_family_v4 = true;
- address_family_v6 = true;
- }
-
- if let Some(interface) = config.interfaces.get_mut(&iface) {
- if address_family_v4 {
- set_method_v4(interface, config_method)?;
- }
- if address_family_v6 {
- set_method_v6(interface, config_method)?;
- }
-
- self.parse_iface_attributes(interface, address_family_v4, address_family_v6)?;
- } else {
- let mut interface = Interface::new(iface.clone());
- if address_family_v4 {
- set_method_v4(&mut interface, config_method)?;
- }
- if address_family_v6 {
- set_method_v6(&mut interface, config_method)?;
- }
-
- self.parse_iface_attributes(&mut interface, address_family_v4, address_family_v6)?;
-
- config.interfaces.insert(interface.name.clone(), interface);
-
- config.order.push(NetworkOrderEntry::Iface(iface));
- }
-
- Ok(())
- }
-
- pub fn parse_interfaces(
- &mut self,
- existing_interfaces: Option<&HashMap<String, bool>>,
- ) -> Result<NetworkConfig, Error> {
- self.do_parse_interfaces(existing_interfaces)
- .map_err(|err| format_err!("line {}: {}", self.line_nr, err))
- }
-
- fn do_parse_interfaces(
- &mut self,
- existing_interfaces: Option<&HashMap<String, bool>>,
- ) -> Result<NetworkConfig, Error> {
- let mut config = NetworkConfig::new();
-
- let mut auto_flag: HashSet<String> = HashSet::new();
-
- loop {
- match self.peek()? {
- Token::EOF => {
- break;
- }
- Token::Newline => {
- // skip empty lines
- self.eat(Token::Newline)?;
- }
- Token::Comment => {
- let (_, text) = self.next()?;
- config.order.push(NetworkOrderEntry::Comment(text));
- self.eat(Token::Newline)?;
- }
- Token::Auto => {
- self.parse_auto(&mut auto_flag)?;
- }
- Token::Iface => {
- self.parse_iface(&mut config)?;
- }
- _ => {
- let option = self.parse_to_eol()?;
- if !option.is_empty() {
- config.order.push(NetworkOrderEntry::Option(option));
- }
- }
- }
- }
-
- for iface in auto_flag.iter() {
- if let Some(interface) = config.interfaces.get_mut(iface) {
- interface.autostart = true;
- }
- }
-
- static INTERFACE_ALIAS_REGEX: LazyLock<Regex> =
- LazyLock::new(|| Regex::new(r"^\S+:\d+$").unwrap());
-
- if let Some(existing_interfaces) = existing_interfaces {
- for (iface, active) in existing_interfaces.iter() {
- if let Some(interface) = config.interfaces.get_mut(iface) {
- interface.active = *active;
- if interface.interface_type == NetworkInterfaceType::Unknown
- && super::is_physical_nic(iface)
- {
- interface.interface_type = NetworkInterfaceType::Eth;
- }
- } else if super::is_physical_nic(iface) {
- // also add all physical NICs
- let mut interface = Interface::new(iface.clone());
- set_method_v4(&mut interface, NetworkConfigMethod::Manual)?;
- interface.interface_type = NetworkInterfaceType::Eth;
- interface.active = *active;
- config.interfaces.insert(interface.name.clone(), interface);
- config
- .order
- .push(NetworkOrderEntry::Iface(iface.to_string()));
- }
- }
- }
-
- for (name, interface) in config.interfaces.iter_mut() {
- if interface.interface_type != NetworkInterfaceType::Unknown {
- continue;
- }
- if name == "lo" {
- interface.interface_type = NetworkInterfaceType::Loopback;
- continue;
- }
- if INTERFACE_ALIAS_REGEX.is_match(name) {
- interface.interface_type = NetworkInterfaceType::Alias;
- continue;
- }
- if VLAN_INTERFACE_REGEX.is_match(name) {
- interface.interface_type = NetworkInterfaceType::Vlan;
- continue;
- }
- if super::is_physical_nic(name) {
- interface.interface_type = NetworkInterfaceType::Eth;
- continue;
- }
- }
-
- if !config.interfaces.contains_key("lo") {
- let mut interface = Interface::new(String::from("lo"));
- set_method_v4(&mut interface, NetworkConfigMethod::Loopback)?;
- interface.interface_type = NetworkInterfaceType::Loopback;
- interface.autostart = true;
- config.interfaces.insert(interface.name.clone(), interface);
-
- // Note: insert 'lo' as first interface after initial comments
- let mut new_order = Vec::new();
- let mut added_lo = false;
- for entry in config.order {
- if added_lo {
- new_order.push(entry);
- continue;
- } // copy the rest
- match entry {
- NetworkOrderEntry::Comment(_) => {
- new_order.push(entry);
- }
- _ => {
- new_order.push(NetworkOrderEntry::Iface(String::from("lo")));
- added_lo = true;
- new_order.push(entry);
- }
- }
- }
- config.order = new_order;
- }
-
- Ok(config)
- }
-}
-
-#[cfg(test)]
-mod test {
-
- use anyhow::Error;
-
- use super::*;
-
- #[test]
- fn test_network_config_create_lo_1() -> Result<(), Error> {
- let input = "";
-
- let mut parser = NetworkParser::new(input.as_bytes());
-
- let config = parser.parse_interfaces(None)?;
-
- let output = String::try_from(config)?;
-
- let expected = "auto lo\niface lo inet loopback\n\n";
- assert_eq!(output, expected);
-
- // run again using output as input
- let mut parser = NetworkParser::new(output.as_bytes());
-
- let config = parser.parse_interfaces(None)?;
-
- let output = String::try_from(config)?;
-
- assert_eq!(output, expected);
-
- Ok(())
- }
-
- #[test]
- fn test_network_config_create_lo_2() -> Result<(), Error> {
- let input = "#c1\n\n#c2\n\niface test inet manual\n";
-
- let mut parser = NetworkParser::new(input.as_bytes());
-
- let config = parser.parse_interfaces(None)?;
-
- let output = String::try_from(config)?;
-
- // Note: loopback should be added in front of other interfaces
- let expected = "#c1\n#c2\n\nauto lo\niface lo inet loopback\n\niface test inet manual\n\n";
- assert_eq!(output, expected);
-
- Ok(())
- }
-
- #[test]
- fn test_network_config_parser_no_blank_1() -> Result<(), Error> {
- let input = "auto lo\n\
- iface lo inet loopback\n\
- iface lo inet6 loopback\n\
- auto ens18\n\
- iface ens18 inet static\n\
- \taddress 192.168.20.144/20\n\
- \tgateway 192.168.16.1\n\
- # comment\n\
- iface ens20 inet static\n\
- \taddress 192.168.20.145/20\n\
- iface ens21 inet manual\n\
- iface ens22 inet manual\n";
-
- let mut parser = NetworkParser::new(input.as_bytes());
-
- let config = parser.parse_interfaces(None)?;
-
- let output = String::try_from(config)?;
-
- let expected = "auto lo\n\
- iface lo inet loopback\n\
- \n\
- iface lo inet6 loopback\n\
- \n\
- auto ens18\n\
- iface ens18 inet static\n\
- \taddress 192.168.20.144/20\n\
- \tgateway 192.168.16.1\n\
- #comment\n\
- \n\
- iface ens20 inet static\n\
- \taddress 192.168.20.145/20\n\
- \n\
- iface ens21 inet manual\n\
- \n\
- iface ens22 inet manual\n\
- \n";
- assert_eq!(output, expected);
-
- Ok(())
- }
-
- #[test]
- fn test_network_config_parser_no_blank_2() -> Result<(), Error> {
- // Adapted from bug 2926
- let input = "### Hetzner Online GmbH installimage\n\
- \n\
- source /etc/network/interfaces.d/*\n\
- \n\
- auto lo\n\
- iface lo inet loopback\n\
- iface lo inet6 loopback\n\
- \n\
- auto enp4s0\n\
- iface enp4s0 inet static\n\
- \taddress 10.10.10.10/24\n\
- \tgateway 10.10.10.1\n\
- \t# route 10.10.20.10/24 via 10.10.20.1\n\
- \tup route add -net 10.10.20.10 netmask 255.255.255.0 gw 10.10.20.1 dev enp4s0\n\
- \n\
- iface enp4s0 inet6 static\n\
- \taddress fe80::5496:35ff:fe99:5a6a/64\n\
- \tgateway fe80::1\n";
-
- let mut parser = NetworkParser::new(input.as_bytes());
-
- let config = parser.parse_interfaces(None)?;
-
- let output = String::try_from(config)?;
-
- let expected = "### Hetzner Online GmbH installimage\n\
- \n\
- source /etc/network/interfaces.d/*\n\
- \n\
- auto lo\n\
- iface lo inet loopback\n\
- \n\
- iface lo inet6 loopback\n\
- \n\
- auto enp4s0\n\
- iface enp4s0 inet static\n\
- \taddress 10.10.10.10/24\n\
- \tgateway 10.10.10.1\n\
- \t# route 10.10.20.10/24 via 10.10.20.1\n\
- \tup route add -net 10.10.20.10 netmask 255.255.255.0 gw 10.10.20.1 dev enp4s0\n\
- \n\
- iface enp4s0 inet6 static\n\
- \taddress fe80::5496:35ff:fe99:5a6a/64\n\
- \tgateway fe80::1\n\
- \n";
- assert_eq!(output, expected);
-
- Ok(())
- }
-
- #[test]
- fn test_network_config_parser_vlan_id_in_name() {
- let input = "iface vmbr0.100 inet static manual";
- let mut parser = NetworkParser::new(input.as_bytes());
- let config = parser.parse_interfaces(None).unwrap();
-
- let iface = config.interfaces.get("vmbr0.100").unwrap();
- assert_eq!(iface.interface_type, NetworkInterfaceType::Vlan);
- assert_eq!(iface.vlan_raw_device, None);
- assert_eq!(iface.vlan_id, None);
- }
-
- #[test]
- fn test_network_config_parser_vlan_with_raw_device() {
- let input = r#"
-iface vlan100 inet manual
- vlan-raw-device vmbr0"#;
-
- let mut parser = NetworkParser::new(input.as_bytes());
- let config = parser.parse_interfaces(None).unwrap();
-
- let iface = config.interfaces.get("vlan100").unwrap();
- assert_eq!(iface.interface_type, NetworkInterfaceType::Vlan);
- assert_eq!(iface.vlan_raw_device, Some(String::from("vmbr0")));
- assert_eq!(iface.vlan_id, None);
- }
-
- #[test]
- fn test_network_config_parser_vlan_with_raw_device_static() {
- let input = r#"
-iface vlan100 inet static
- vlan-raw-device vmbr0
- address 10.0.0.100/16"#;
-
- let mut parser = NetworkParser::new(input.as_bytes());
- let config = parser.parse_interfaces(None).unwrap();
-
- let iface = config.interfaces.get("vlan100").unwrap();
- assert_eq!(iface.interface_type, NetworkInterfaceType::Vlan);
- assert_eq!(iface.vlan_raw_device, Some(String::from("vmbr0")));
- assert_eq!(iface.vlan_id, None);
- assert_eq!(iface.method, Some(NetworkConfigMethod::Static));
- assert_eq!(iface.cidr, Some(String::from("10.0.0.100/16")));
- }
-
- #[test]
- fn test_network_config_parser_vlan_individual_name() {
- let input = r#"
-iface individual_name inet manual
- vlan-id 100
- vlan-raw-device vmbr0"#;
-
- let mut parser = NetworkParser::new(input.as_bytes());
- let config = parser.parse_interfaces(None).unwrap();
-
- let iface = config.interfaces.get("individual_name").unwrap();
- assert_eq!(iface.interface_type, NetworkInterfaceType::Vlan);
- assert_eq!(iface.vlan_raw_device, Some(String::from("vmbr0")));
- assert_eq!(iface.vlan_id, Some(100));
- }
-
- #[test]
- fn test_network_config_parser_vlan_individual_name_static() {
- let input = r#"
-iface individual_name inet static
- vlan-id 100
- vlan-raw-device vmbr0
- address 10.0.0.100/16
-"#;
-
- let mut parser = NetworkParser::new(input.as_bytes());
- let config = parser.parse_interfaces(None).unwrap();
-
- let iface = config.interfaces.get("individual_name").unwrap();
- assert_eq!(iface.interface_type, NetworkInterfaceType::Vlan);
- assert_eq!(iface.vlan_raw_device, Some(String::from("vmbr0")));
- assert_eq!(iface.vlan_id, Some(100));
- assert_eq!(iface.method, Some(NetworkConfigMethod::Static));
- assert_eq!(iface.cidr, Some(String::from("10.0.0.100/16")));
- }
-}
diff --git a/src/api2/node/network.rs b/src/api2/node/network.rs
index 273751c4..4ee0231d 100644
--- a/src/api2/node/network.rs
+++ b/src/api2/node/network.rs
@@ -7,13 +7,14 @@ use proxmox_router::{ApiMethod, Permission, Router, RpcEnvironment};
use proxmox_schema::api;
use pbs_api_types::{
- Authid, BondXmitHashPolicy, Interface, LinuxBondMode, NetworkConfigMethod,
- NetworkInterfaceType, CIDR_V4_SCHEMA, CIDR_V6_SCHEMA, IP_V4_SCHEMA, IP_V6_SCHEMA,
- NETWORK_INTERFACE_ARRAY_SCHEMA, NETWORK_INTERFACE_LIST_SCHEMA, NETWORK_INTERFACE_NAME_SCHEMA,
- NODE_SCHEMA, PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, PROXMOX_CONFIG_DIGEST_SCHEMA,
+ Authid, NODE_SCHEMA, PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, PROXMOX_CONFIG_DIGEST_SCHEMA,
};
-use pbs_config::network::{
- self, parse_vlan_id_from_name, parse_vlan_raw_device_from_name, NetworkConfig,
+
+use proxmox_network_api::{
+ self as network, parse_vlan_id_from_name, parse_vlan_raw_device_from_name, BondXmitHashPolicy,
+ Interface, LinuxBondMode, NetworkConfig, NetworkConfigMethod, NetworkInterfaceType,
+ CIDR_V4_SCHEMA, CIDR_V6_SCHEMA, IP_V4_SCHEMA, IP_V6_SCHEMA, NETWORK_INTERFACE_ARRAY_SCHEMA,
+ NETWORK_INTERFACE_LIST_SCHEMA, NETWORK_INTERFACE_NAME_SCHEMA,
};
use proxmox_rest_server::WorkerTask;
diff --git a/src/bin/proxmox-backup-api.rs b/src/bin/proxmox-backup-api.rs
index 74528236..9a8c3c51 100644
--- a/src/bin/proxmox-backup-api.rs
+++ b/src/bin/proxmox-backup-api.rs
@@ -74,10 +74,11 @@ async fn run() -> Result<(), Error> {
proxmox_backup::auth_helpers::setup_auth_context(true);
proxmox_backup::server::notifications::init()?;
-
let backup_user = pbs_config::backup_user()?;
let mut command_sock = proxmox_daemon::command_socket::CommandSocket::new(backup_user.gid);
+ proxmox_product_config::init(backup_user.clone(), pbs_config::priv_user()?);
+
let dir_opts = CreateOptions::new()
.owner(backup_user.uid)
.group(backup_user.gid);
diff --git a/src/bin/proxmox-backup-manager.rs b/src/bin/proxmox-backup-manager.rs
index 0a04ce0b..a4617203 100644
--- a/src/bin/proxmox-backup-manager.rs
+++ b/src/bin/proxmox-backup-manager.rs
@@ -659,6 +659,7 @@ async fn run() -> Result<(), Error> {
.tasklog_pbs()
.init()?;
proxmox_backup::server::notifications::init()?;
+ proxmox_product_config::init(pbs_config::backup_user()?, pbs_config::priv_user()?);
let cmd_def = CliCommandMap::new()
.insert("acl", acl_commands())
diff --git a/src/bin/proxmox-backup-proxy.rs b/src/bin/proxmox-backup-proxy.rs
index 4641fed1..762f2325 100644
--- a/src/bin/proxmox-backup-proxy.rs
+++ b/src/bin/proxmox-backup-proxy.rs
@@ -188,6 +188,7 @@ async fn run() -> Result<(), Error> {
proxmox_backup::auth_helpers::setup_auth_context(false);
proxmox_backup::server::notifications::init()?;
metric_collection::init()?;
+ proxmox_product_config::init(pbs_config::backup_user()?, pbs_config::priv_user()?);
let mut indexpath = PathBuf::from(pbs_buildcfg::JS_DIR);
indexpath.push("index.hbs");
diff --git a/src/bin/proxmox_backup_manager/network.rs b/src/bin/proxmox_backup_manager/network.rs
index 0f0a50a8..d6d990a5 100644
--- a/src/bin/proxmox_backup_manager/network.rs
+++ b/src/bin/proxmox_backup_manager/network.rs
@@ -159,25 +159,25 @@ pub fn network_commands() -> CommandLineInterface {
CliCommand::new(&api2::node::network::API_METHOD_CREATE_INTERFACE)
.fixed_param("node", String::from("localhost"))
.arg_param(&["iface"])
- .completion_cb("iface", pbs_config::network::complete_interface_name)
- .completion_cb("bridge_ports", pbs_config::network::complete_port_list)
- .completion_cb("slaves", pbs_config::network::complete_port_list),
+ .completion_cb("iface", proxmox_network_api::complete_interface_name)
+ .completion_cb("bridge_ports", proxmox_network_api::complete_port_list)
+ .completion_cb("slaves", proxmox_network_api::complete_port_list),
)
.insert(
"update",
CliCommand::new(&api2::node::network::API_METHOD_UPDATE_INTERFACE)
.fixed_param("node", String::from("localhost"))
.arg_param(&["iface"])
- .completion_cb("iface", pbs_config::network::complete_interface_name)
- .completion_cb("bridge_ports", pbs_config::network::complete_port_list)
- .completion_cb("slaves", pbs_config::network::complete_port_list),
+ .completion_cb("iface", proxmox_network_api::complete_interface_name)
+ .completion_cb("bridge_ports", proxmox_network_api::complete_port_list)
+ .completion_cb("slaves", proxmox_network_api::complete_port_list),
)
.insert(
"remove",
CliCommand::new(&api2::node::network::API_METHOD_DELETE_INTERFACE)
.fixed_param("node", String::from("localhost"))
.arg_param(&["iface"])
- .completion_cb("iface", pbs_config::network::complete_interface_name),
+ .completion_cb("iface", proxmox_network_api::complete_interface_name),
)
.insert(
"revert",
--
2.47.2
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
^ permalink raw reply [flat|nested] 16+ messages in thread
* [pbs-devel] [PATCH proxmox-backup 2/2] metric_collection: use ip link for determining the type of interfaces
2025-07-29 16:56 [pbs-devel] [PATCH proxmox{-ve-rs, , -backup, -firewall, -network-interface-pinning} 0/8] proxmox-network-interface-pinning Stefan Hanreich
` (4 preceding siblings ...)
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox-backup 1/2] config: network: move to proxmox-network-api Stefan Hanreich
@ 2025-07-29 16:56 ` Stefan Hanreich
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox-firewall 1/1] firewall: config: use proxmox-network-api Stefan Hanreich
` (2 subsequent siblings)
8 siblings, 0 replies; 16+ messages in thread
From: Stefan Hanreich @ 2025-07-29 16:56 UTC (permalink / raw)
To: pbs-devel
Physical interfaces now can have arbitrary names with the introduction
of proxmox-network-interface pinning. In order to correctly report the
bandwidth metrics for PBS hosts, the method for determining whether an
interface is physical or not needs to be adjusted as well. Use the
adapted helper from proxmox-network-api to query the information about
interfaces and use that to determine the type of interface at every
site that uses the generated stats. To avoid spawning a new process
with every update loop, cache the initial query and use that
information.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
src/server/metric_collection/mod.rs | 87 +++++++++++++++++---
src/server/metric_collection/pull_metrics.rs | 5 +-
src/server/metric_collection/rrd.rs | 5 +-
3 files changed, 79 insertions(+), 18 deletions(-)
diff --git a/src/server/metric_collection/mod.rs b/src/server/metric_collection/mod.rs
index daedfb72..9b62cbb4 100644
--- a/src/server/metric_collection/mod.rs
+++ b/src/server/metric_collection/mod.rs
@@ -1,7 +1,8 @@
use std::{
+ collections::HashMap,
path::Path,
pin::pin,
- sync::Arc,
+ sync::{Arc, OnceLock},
time::{Duration, Instant},
};
@@ -9,6 +10,7 @@ use anyhow::Error;
use tokio::join;
use pbs_api_types::{DataStoreConfig, Operation};
+use proxmox_network_api::{get_network_interfaces, IpLink};
use proxmox_sys::{
fs::FileSystemInformation,
linux::procfs::{Loadavg, ProcFsMemInfo, ProcFsNetDev, ProcFsStat},
@@ -101,7 +103,7 @@ async fn run_stat_generator() {
struct HostStats {
proc: Option<ProcFsStat>,
meminfo: Option<ProcFsMemInfo>,
- net: Option<Vec<ProcFsNetDev>>,
+ net: Option<Vec<NetdevStat>>,
load: Option<Loadavg>,
}
@@ -111,11 +113,78 @@ struct DiskStat {
dev: Option<BlockDevStat>,
}
-fn collect_host_stats_sync() -> HostStats {
- use proxmox_sys::linux::procfs::{
- read_loadavg, read_meminfo, read_proc_net_dev, read_proc_stat,
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
+enum NetdevType {
+ Physical,
+ Virtual,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
+struct NetdevStat {
+ pub device: String,
+ pub receive: u64,
+ pub send: u64,
+ pub ty: NetdevType,
+}
+
+impl NetdevStat {
+ fn from_fs_net_dev(net_dev: ProcFsNetDev, ty: NetdevType) -> Self {
+ Self {
+ device: net_dev.device,
+ receive: net_dev.receive,
+ send: net_dev.send,
+ ty,
+ }
+ }
+}
+
+static NETWORK_INTERFACE_CACHE: OnceLock<HashMap<String, IpLink>> = OnceLock::new();
+
+fn collect_netdev_stats() -> Option<Vec<NetdevStat>> {
+ use proxmox_sys::linux::procfs::read_proc_net_dev;
+
+ let net_devs = match read_proc_net_dev() {
+ Ok(net_devs) => net_devs,
+ Err(err) => {
+ eprintln!("read_prox_net_dev failed - {err}");
+ return None;
+ }
};
+ let ip_links = match NETWORK_INTERFACE_CACHE.get() {
+ Some(ip_links) => ip_links,
+ None => match get_network_interfaces() {
+ Ok(network_interfaces) => {
+ let _ = NETWORK_INTERFACE_CACHE.set(network_interfaces);
+ NETWORK_INTERFACE_CACHE.get().unwrap()
+ }
+ Err(err) => {
+ eprintln!("get_network_interfaces failed - {err}");
+ return None;
+ }
+ },
+ };
+
+ let mut stat_devs = Vec::with_capacity(net_devs.len());
+
+ for net_dev in net_devs {
+ if let Some(ip_link) = ip_links.get(&net_dev.device) {
+ let ty = if ip_link.is_physical() {
+ NetdevType::Physical
+ } else {
+ NetdevType::Virtual
+ };
+
+ stat_devs.push(NetdevStat::from_fs_net_dev(net_dev, ty));
+ }
+ }
+
+ Some(stat_devs)
+}
+
+fn collect_host_stats_sync() -> HostStats {
+ use proxmox_sys::linux::procfs::{read_loadavg, read_meminfo, read_proc_stat};
+
let proc = match read_proc_stat() {
Ok(stat) => Some(stat),
Err(err) => {
@@ -132,13 +201,7 @@ fn collect_host_stats_sync() -> HostStats {
}
};
- let net = match read_proc_net_dev() {
- Ok(netdev) => Some(netdev),
- Err(err) => {
- eprintln!("read_prox_net_dev failed - {err}");
- None
- }
- };
+ let net = collect_netdev_stats();
let load = match read_loadavg() {
Ok(loadavg) => Some(loadavg),
diff --git a/src/server/metric_collection/pull_metrics.rs b/src/server/metric_collection/pull_metrics.rs
index 3b105eaf..e99662fa 100644
--- a/src/server/metric_collection/pull_metrics.rs
+++ b/src/server/metric_collection/pull_metrics.rs
@@ -12,7 +12,7 @@ use proxmox_shared_cache::SharedCache;
use proxmox_sys::fs::CreateOptions;
use serde::{Deserialize, Serialize};
-use super::{DiskStat, HostStats, METRIC_COLLECTION_INTERVAL};
+use super::{DiskStat, HostStats, NetdevType, METRIC_COLLECTION_INTERVAL};
const METRIC_CACHE_TIME: Duration = Duration::from_secs(30 * 60);
const STORED_METRIC_GENERATIONS: u64 =
@@ -113,11 +113,10 @@ pub(super) fn update_metrics(
}
if let Some(netdev) = &host.net {
- use pbs_config::network::is_physical_nic;
let mut netin = 0;
let mut netout = 0;
for item in netdev {
- if !is_physical_nic(&item.device) {
+ if item.ty != NetdevType::Physical {
continue;
}
netin += item.receive;
diff --git a/src/server/metric_collection/rrd.rs b/src/server/metric_collection/rrd.rs
index ed39cc94..d129432e 100644
--- a/src/server/metric_collection/rrd.rs
+++ b/src/server/metric_collection/rrd.rs
@@ -16,7 +16,7 @@ use proxmox_sys::fs::CreateOptions;
use pbs_buildcfg::PROXMOX_BACKUP_STATE_DIR_M;
use proxmox_rrd_api_types::{RrdMode, RrdTimeframe};
-use super::{DiskStat, HostStats};
+use super::{DiskStat, HostStats, NetdevType};
const RRD_CACHE_BASEDIR: &str = concat!(PROXMOX_BACKUP_STATE_DIR_M!(), "/rrdb");
@@ -162,11 +162,10 @@ pub(super) fn update_metrics(host: &HostStats, hostdisk: &DiskStat, datastores:
}
if let Some(netdev) = &host.net {
- use pbs_config::network::is_physical_nic;
let mut netin = 0;
let mut netout = 0;
for item in netdev {
- if !is_physical_nic(&item.device) {
+ if item.ty != NetdevType::Physical {
continue;
}
netin += item.receive;
--
2.47.2
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
^ permalink raw reply [flat|nested] 16+ messages in thread
* [pbs-devel] [PATCH proxmox-firewall 1/1] firewall: config: use proxmox-network-api
2025-07-29 16:56 [pbs-devel] [PATCH proxmox{-ve-rs, , -backup, -firewall, -network-interface-pinning} 0/8] proxmox-network-interface-pinning Stefan Hanreich
` (5 preceding siblings ...)
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox-backup 2/2] metric_collection: use ip link for determining the type of interfaces Stefan Hanreich
@ 2025-07-29 16:56 ` Stefan Hanreich
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox-network-interface-pinning 1/1] initial commit Stefan Hanreich
2025-07-30 14:37 ` [pbs-devel] superseded: [PATCH proxmox{-ve-rs, , -backup, -firewall, -network-interface-pinning} 0/8] proxmox-network-interface-pinning Stefan Hanreich
8 siblings, 0 replies; 16+ messages in thread
From: Stefan Hanreich @ 2025-07-29 16:56 UTC (permalink / raw)
To: pbs-devel
proxmox-network-api now provides functions for obtaining the network
interface information directly. Adapt the firewall to use the function
from proxmox-network-api instead.
The name of InterfaceMapping has changed during this, so adapt the
firewall to use the new name for the struct.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
proxmox-firewall/Cargo.toml | 3 ++-
proxmox-firewall/src/config.rs | 29 +++++----------------
proxmox-firewall/tests/integration_tests.rs | 8 +++---
3 files changed, 12 insertions(+), 28 deletions(-)
diff --git a/proxmox-firewall/Cargo.toml b/proxmox-firewall/Cargo.toml
index 24f4dc6..468e2b5 100644
--- a/proxmox-firewall/Cargo.toml
+++ b/proxmox-firewall/Cargo.toml
@@ -21,8 +21,9 @@ serde_json = "1"
signal-hook = "0.3"
proxmox-log = "1"
-proxmox-nftables = { path = "../proxmox-nftables", features = ["config-ext"] }
proxmox-network-types = { workspace = true }
+proxmox-network-api = { version = "1", features = [ "impl" ] }
+proxmox-nftables = { path = "../proxmox-nftables", features = ["config-ext"] }
proxmox-ve-config = { workspace = true }
[dev-dependencies]
diff --git a/proxmox-firewall/src/config.rs b/proxmox-firewall/src/config.rs
index 65926ea..d6a4df5 100644
--- a/proxmox-firewall/src/config.rs
+++ b/proxmox-firewall/src/config.rs
@@ -15,10 +15,9 @@ use proxmox_ve_config::firewall::types::alias::{Alias, AliasName, AliasScope};
use proxmox_ve_config::guest::types::Vmid;
use proxmox_ve_config::guest::{GuestEntry, GuestMap};
-use proxmox_ve_config::host::network::InterfaceMapping;
-use proxmox_ve_config::host::network::IpLink;
use proxmox_ve_config::host::types::BridgeName;
+use proxmox_network_api::{get_network_interfaces, AltnameMapping};
use proxmox_nftables::command::{CommandOutput, Commands, List, ListOutput};
use proxmox_nftables::types::ListChain;
use proxmox_nftables::NftClient;
@@ -44,7 +43,7 @@ pub trait FirewallConfigLoader {
&self,
bridge_name: &BridgeName,
) -> Result<Option<Box<dyn io::BufRead>>, Error>;
- fn interface_mapping(&self) -> Result<InterfaceMapping, Error>;
+ fn interface_mapping(&self) -> Result<AltnameMapping, Error>;
}
#[derive(Default)]
@@ -227,24 +226,10 @@ impl FirewallConfigLoader for PveFirewallConfigLoader {
Ok(None)
}
- fn interface_mapping(&self) -> Result<InterfaceMapping, Error> {
- let output = std::process::Command::new("ip")
- .arg("-details")
- .arg("-json")
- .arg("link")
- .arg("show")
- .stdout(std::process::Stdio::piped())
- .output()
- .with_context(|| "could not obtain ip link output")?;
-
- if !output.status.success() {
- bail!("ip link returned non-zero exit code")
- }
-
- Ok(serde_json::from_slice::<Vec<IpLink>>(&output.stdout)
- .with_context(|| "could not deserialize ip link output")?
- .into_iter()
- .collect())
+ fn interface_mapping(&self) -> Result<AltnameMapping, Error> {
+ Ok(AltnameMapping::from_iter(
+ get_network_interfaces()?.into_values(),
+ ))
}
}
@@ -280,7 +265,7 @@ pub struct FirewallConfig {
nft_config: BTreeMap<String, ListChain>,
sdn_config: Option<SdnConfig>,
ipam_config: Option<Ipam>,
- interface_mapping: InterfaceMapping,
+ interface_mapping: AltnameMapping,
}
impl FirewallConfig {
diff --git a/proxmox-firewall/tests/integration_tests.rs b/proxmox-firewall/tests/integration_tests.rs
index 69f9cc2..2c550eb 100644
--- a/proxmox-firewall/tests/integration_tests.rs
+++ b/proxmox-firewall/tests/integration_tests.rs
@@ -1,9 +1,9 @@
use anyhow::{Context, Error};
-use proxmox_ve_config::host::network::InterfaceMapping;
use std::collections::HashMap;
use proxmox_firewall::config::{FirewallConfig, FirewallConfigLoader, NftConfigLoader};
use proxmox_firewall::firewall::Firewall;
+use proxmox_network_api::AltnameMapping;
use proxmox_nftables::command::CommandOutput;
use proxmox_sys::nodename;
use proxmox_ve_config::guest::types::Vmid;
@@ -93,10 +93,8 @@ impl FirewallConfigLoader for MockFirewallConfigLoader {
Ok(None)
}
- fn interface_mapping(
- &self,
- ) -> Result<proxmox_ve_config::host::network::InterfaceMapping, Error> {
- Ok(InterfaceMapping::from_iter(vec![]))
+ fn interface_mapping(&self) -> Result<AltnameMapping, Error> {
+ Ok(AltnameMapping::from_iter(vec![]))
}
}
--
2.47.2
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
^ permalink raw reply [flat|nested] 16+ messages in thread
* [pbs-devel] [PATCH proxmox-network-interface-pinning 1/1] initial commit
2025-07-29 16:56 [pbs-devel] [PATCH proxmox{-ve-rs, , -backup, -firewall, -network-interface-pinning} 0/8] proxmox-network-interface-pinning Stefan Hanreich
` (6 preceding siblings ...)
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox-firewall 1/1] firewall: config: use proxmox-network-api Stefan Hanreich
@ 2025-07-29 16:56 ` Stefan Hanreich
2025-07-30 13:07 ` Thomas Lamprecht
2025-07-30 14:37 ` [pbs-devel] superseded: [PATCH proxmox{-ve-rs, , -backup, -firewall, -network-interface-pinning} 0/8] proxmox-network-interface-pinning Stefan Hanreich
8 siblings, 1 reply; 16+ messages in thread
From: Stefan Hanreich @ 2025-07-29 16:56 UTC (permalink / raw)
To: pbs-devel
Introduce proxmox-network-interface-pinning, which is a
reimplementation of the tool from pve-manager. It should function
identically to the PVE version, except for virtual function support.
It also uses the ifupdown2 configuration parser from Rust, instead of
the perl implementation, which might have some subtle differences in
their handling of ifupdown2 configuration files.
In order to support hosts that have both Proxmox VE and Proxmox Backup
Server installed, this tool tries to detect the existence of the
Proxmox VE tool and executes the Proxmox VE tool instead, if it is
present on the host.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
.cargo/config.toml | 5 +
.gitignore | 8 +
Cargo.lock | 1619 ++++++++++++++++++++++++++++++++++++++++++
Cargo.toml | 24 +
Makefile | 86 +++
debian/changelog | 5 +
debian/control | 36 +
debian/copyright | 17 +
debian/debcargo.toml | 8 +
debian/rules | 31 +
src/main.rs | 579 +++++++++++++++
11 files changed, 2418 insertions(+)
create mode 100644 .cargo/config.toml
create mode 100644 .gitignore
create mode 100644 Cargo.lock
create mode 100644 Cargo.toml
create mode 100644 Makefile
create mode 100644 debian/changelog
create mode 100644 debian/control
create mode 100644 debian/copyright
create mode 100644 debian/debcargo.toml
create mode 100755 debian/rules
create mode 100644 src/main.rs
diff --git a/.cargo/config.toml b/.cargo/config.toml
new file mode 100644
index 0000000..3b5b6e4
--- /dev/null
+++ b/.cargo/config.toml
@@ -0,0 +1,5 @@
+[source]
+[source.debian-packages]
+directory = "/usr/share/cargo/registry"
+[source.crates-io]
+replace-with = "debian-packages"
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..26ba170
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+/target
+/proxmox-network-interface-pinning-[0-9]*/
+*.build
+*.buildinfo
+*.changes
+*.deb
+*.dsc
+*.tar*
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..57631ff
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,1619 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "addr2line"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "ahash"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "anstream"
+version = "0.6.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "colorchoice",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "anyhow"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "backtrace"
+version = "0.3.74"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bitflags"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "bumpalo"
+version = "3.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "bytes"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "cc"
+version = "1.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362"
+dependencies = [
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chrono"
+version = "0.4.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "iana-time-zone",
+ "num-traits",
+ "serde",
+]
+
+[[package]]
+name = "colorchoice"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+
+[[package]]
+name = "const_format"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "const_format_proc_macros",
+]
+
+[[package]]
+name = "const_format_proc_macros"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "darling"
+version = "0.20.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.20.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.20.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "deranged"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
+dependencies = [
+ "powerfmt",
+ "serde",
+]
+
+[[package]]
+name = "endian-type"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
+
+[[package]]
+name = "endian_trait"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77c844962d33db56fe7024846eeb8db92c79ccb68d3752a0ee37c261ac79fd46"
+dependencies = [
+ "endian_trait_derive",
+]
+
+[[package]]
+name = "endian_trait_derive"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "env_filter"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
+dependencies = [
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "env_filter",
+ "humantime",
+ "log",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "errno"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "fd-lock"
+version = "3.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "cfg-if",
+ "rustix",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "futures"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "h2"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "ahash",
+ "allocator-api2",
+]
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "home"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "http"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
+dependencies = [
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "httpdate"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "hyper"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.63"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "indexmap"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+ "serde",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
+
+[[package]]
+name = "js-sys"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "libc"
+version = "0.2.169"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
+
+[[package]]
+name = "log"
+version = "0.4.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "memoffset"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "nibble_vec"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43"
+dependencies = [
+ "smallvec",
+]
+
+[[package]]
+name = "nix"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+ "memoffset",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "object"
+version = "0.36.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "openssl"
+version = "0.10.72"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "proxmox-api-macro"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "anyhow",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "proxmox-async"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "anyhow",
+ "futures",
+ "pin-utils",
+ "proxmox-io",
+ "proxmox-lang",
+ "tokio",
+]
+
+[[package]]
+name = "proxmox-config-digest"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "anyhow",
+ "hex",
+ "openssl",
+ "proxmox-schema",
+ "serde",
+ "serde_plain",
+]
+
+[[package]]
+name = "proxmox-http"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "anyhow",
+ "bytes",
+ "futures",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "sync_wrapper",
+]
+
+[[package]]
+name = "proxmox-http-error"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "anyhow",
+ "http",
+ "serde",
+]
+
+[[package]]
+name = "proxmox-io"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "endian_trait",
+ "tokio",
+]
+
+[[package]]
+name = "proxmox-lang"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "proxmox-log"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "anyhow",
+ "nix",
+ "proxmox-sys",
+ "proxmox-time",
+ "tokio",
+ "tracing",
+ "tracing-journald",
+ "tracing-log",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "proxmox-network-api"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "anyhow",
+ "const_format",
+ "libc",
+ "nix",
+ "proxmox-config-digest",
+ "proxmox-network-types",
+ "proxmox-product-config",
+ "proxmox-schema",
+ "proxmox-sys",
+ "regex",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "proxmox-network-interface-pinning"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "nix",
+ "proxmox-async",
+ "proxmox-log",
+ "proxmox-network-api",
+ "proxmox-network-types",
+ "proxmox-product-config",
+ "proxmox-router",
+ "proxmox-schema",
+ "serde",
+ "serde_json",
+ "walkdir",
+]
+
+[[package]]
+name = "proxmox-network-types"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "serde",
+ "serde_with",
+ "thiserror",
+]
+
+[[package]]
+name = "proxmox-product-config"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "anyhow",
+ "nix",
+ "proxmox-sys",
+]
+
+[[package]]
+name = "proxmox-router"
+version = "3.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "anyhow",
+ "bytes",
+ "env_logger",
+ "futures",
+ "http",
+ "hyper",
+ "libc",
+ "nix",
+ "percent-encoding",
+ "proxmox-async",
+ "proxmox-http",
+ "proxmox-http-error",
+ "proxmox-schema",
+ "rustyline",
+ "serde",
+ "serde_json",
+ "serde_plain",
+ "unicode-width",
+]
+
+[[package]]
+name = "proxmox-schema"
+version = "4.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "anyhow",
+ "const_format",
+ "proxmox-api-macro",
+ "regex",
+ "serde",
+ "serde_json",
+ "textwrap",
+]
+
+[[package]]
+name = "proxmox-sys"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "anyhow",
+ "libc",
+ "log",
+ "nix",
+ "proxmox-io",
+ "proxmox-lang",
+ "regex",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "proxmox-time"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "anyhow",
+ "bitflags",
+ "js-sys",
+ "libc",
+ "nom",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "radix_trie"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd"
+dependencies = [
+ "endian-type",
+ "nibble_vec",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+
+[[package]]
+name = "rustix"
+version = "0.38.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+]
+
+[[package]]
+name = "rustyline"
+version = "14.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "fd-lock",
+ "home",
+ "libc",
+ "log",
+ "memchr",
+ "nix",
+ "radix_trie",
+ "unicode-segmentation",
+ "unicode-width",
+ "utf8parse",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.217"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.217"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.139"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_plain"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95455e7e29fada2052e72170af226fbe368a4ca33dee847875325d9fdb133858"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_with"
+version = "3.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "base64",
+ "chrono",
+ "hex",
+ "indexmap",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "serde_with_macros",
+ "time",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "3.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "smawk"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "socket2"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.98"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "smawk",
+ "unicode-linebreak",
+ "unicode-width",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "time"
+version = "0.3.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "time-macros"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tokio"
+version = "1.43.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-journald"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "libc",
+ "tracing-core",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "nu-ansi-term",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
+
+[[package]]
+name = "unicode-linebreak"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "hashbrown",
+ "regex",
+]
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
+[[package]]
+name = "valuable"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "log",
+ "try-lock",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "zerocopy"
+version = "0.7.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "Could not get crate checksum"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..017d2ad
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "proxmox-network-interface-pinning"
+version = "0.1.0"
+authors = ["Stefan Hanreich <s.hanreich@proxmox.com>"]
+edition = "2024"
+license = "AGPL-3"
+description = "Tool for pinning the name of network interfaces."
+
+exclude = ["debian"]
+
+[dependencies]
+anyhow = "1.0.95"
+nix = "0.29"
+serde = "1.0.217"
+serde_json = "1.0.139"
+walkdir = "2.5.0"
+
+proxmox-async = "0.5.0"
+proxmox-log = "1.0.0"
+proxmox-network-api = { version = "1.0.0", features = [ "impl" ] }
+proxmox-network-types = "0.1.0"
+proxmox-product-config = "1"
+proxmox-router = "3.2.2"
+proxmox-schema = { version = "4.1.1", features = [ "api-macro" ] }
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..a6bf0cc
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,86 @@
+include /usr/share/dpkg/default.mk
+
+PACKAGE=proxmox-network-interface-pinning
+CRATENAME=proxmox-network-interface-pinning
+
+BUILDDIR ?= $(PACKAGE)-$(DEB_VERSION_UPSTREAM)
+ORIG_SRC_TAR=$(PACKAGE)_$(DEB_VERSION_UPSTREAM).orig.tar.gz
+
+DEB=$(PACKAGE)_$(DEB_VERSION)_$(DEB_HOST_ARCH).deb
+DBG_DEB=$(PACKAGE)-dbgsym_$(DEB_VERSION)_$(DEB_HOST_ARCH).deb
+DSC=$(PACKAGE)_$(DEB_VERSION).dsc
+
+CARGO ?= cargo
+ifeq ($(BUILD_MODE), release)
+CARGO_BUILD_ARGS += --release
+COMPILEDIR := target/release
+else
+COMPILEDIR := target/debug
+endif
+
+PREFIX = /usr
+LIBEXECDIR = $(PREFIX)/libexec
+PROXMOX_LIBEXECDIR = $(LIBEXECDIR)/proxmox
+
+PROXMOX_NETWORK_INTERFACE_PINNING_BIN := $(addprefix $(COMPILEDIR)/,proxmox-network-interface-pinning)
+
+all:
+
+install: $(PROXMOX_NETWORK_INTERFACE_PINNING_BIN)
+ install -dm755 $(DESTDIR)$(PROXMOX_LIBEXECDIR)
+ install -m755 $(PROXMOX_NETWORK_INTERFACE_PINNING_BIN) $(DESTDIR)$(PROXMOX_LIBEXECDIR)/
+
+$(PROXMOX_NETWORK_INTERFACE_PINNING_BIN): .do-cargo-build
+.do-cargo-build:
+ $(CARGO) build $(CARGO_BUILD_ARGS)
+ touch .do-cargo-build
+
+
+.PHONY: cargo-build
+cargo-build: .do-cargo-build
+
+$(BUILDDIR):
+ rm -rf $@ $@.tmp
+ mkdir $@.tmp
+ cp -a debian/ src/ Makefile Cargo.toml $@.tmp
+ mv $@.tmp $@
+
+
+$(ORIG_SRC_TAR): $(BUILDDIR)
+ tar czf $(ORIG_SRC_TAR) --exclude="$(BUILDDIR)/debian" $(BUILDDIR)
+
+.PHONY: deb
+deb: $(DEB)
+$(DEB) $(DBG_DEB) &: $(BUILDDIR)
+ cd $(BUILDDIR); dpkg-buildpackage -b -uc -us
+ lintian $(DEB)
+ @echo $(DEB)
+
+.PHONY: dsc
+dsc:
+ rm -rf $(DSC) $(BUILDDIR)
+ $(MAKE) $(DSC)
+ lintian $(DSC)
+
+$(DSC): $(BUILDDIR) $(ORIG_SRC_TAR)
+ cd $(BUILDDIR); dpkg-buildpackage -S -us -uc -d
+
+sbuild: $(DSC)
+ sbuild $(DSC)
+
+.PHONY: upload
+upload: UPLOAD_DIST ?= $(DEB_DISTRIBUTION)
+upload: $(DEB) $(DBG_DEB)
+ tar cf - $(DEB) $(DBG_DEB) |ssh -X repoman@repo.proxmox.com -- upload --product pbs --dist $(UPLOAD_DIST) --arch $(DEB_HOST_ARCH)
+
+.PHONY: clean distclean
+distclean: clean
+clean:
+ $(CARGO) clean
+ rm -rf $(PACKAGE)-[0-9]*/ build/
+ rm -f *.deb *.changes *.dsc *.tar.* *.buildinfo *.build .do-cargo-build
+
+.PHONY: dinstall
+dinstall: deb
+ dpkg -i $(DEB)
+
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..56422e9
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,5 @@
+proxmox-network-interface-pinning (0.1.0) trixie; urgency=medium
+
+ * Initial release.
+
+ -- Proxmox Support Team <support@proxmox.com> Tue, 29 Jul 2025 14:39:57 +0200
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..5d3edf2
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,36 @@
+Source: proxmox-network-interface-pinning
+Section: admin
+Priority: optional
+Build-Depends: debhelper-compat (= 13),
+ dh-sequence-cargo,
+ cargo:native,
+ rustc:native,
+ libstd-rust-dev,
+ librust-anyhow-1+default-dev (>= 1.0.95-~~),
+ librust-nix-0.29+default-dev,
+ librust-proxmox-async-0.5+default-dev,
+ librust-proxmox-log-1+default-dev,
+ librust-proxmox-network-api-1+default-dev,
+ librust-proxmox-network-api-1+impl-dev,
+ librust-proxmox-network-types-0.1+default-dev,
+ librust-proxmox-product-config+default-dev,
+ librust-proxmox-router-3+default-dev (>= 3.2.2-~~),
+ librust-proxmox-schema-4+api-macro-dev (>= 4.1.1-~~),
+ librust-proxmox-schema-4+default-dev (>= 4.1.1-~~),
+ librust-serde-1+default-dev (>= 1.0.217-~~),
+ librust-serde-json-1+default-dev (>= 1.0.139-~~),
+ librust-walkdir-2+default-dev (>= 2.5.0-~~)
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Standards-Version: 4.7.0
+Vcs-Git:
+Vcs-Browser:
+Rules-Requires-Root: no
+
+Package: proxmox-network-interface-pinning
+Architecture: any
+Multi-Arch: allowed
+Depends: ${misc:Depends}, ${shlibs:Depends},
+Description: Pinning the name of network interfaces
+ This package contains the following binaries built from the Rust crate
+ "proxmox-network-interface-pinning":
+ - proxmox-network-interface-pinning
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..61c573d
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,17 @@
+Copyright (C) 2025 Proxmox Server Solutions GmbH
+
+This software is written by Proxmox Server Solutions GmbH <support@proxmox.com>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+
diff --git a/debian/debcargo.toml b/debian/debcargo.toml
new file mode 100644
index 0000000..703440f
--- /dev/null
+++ b/debian/debcargo.toml
@@ -0,0 +1,8 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@proxmox.com>"
+
+[source]
+# TODO: update once public
+vcs_git = ""
+vcs_browser = ""
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..e157e13
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,31 @@
+#!/usr/bin/make -f
+# See debhelper(7) (uncomment to enable)
+# output every command that modifies files on the build system.
+DH_VERBOSE = 1
+
+include /usr/share/dpkg/pkg-info.mk
+include /usr/share/rustc/architecture.mk
+
+export BUILD_MODE=release
+
+CARGO=/usr/share/cargo/bin/cargo
+
+export CFLAGS CXXFLAGS CPPFLAGS LDFLAGS
+export DEB_HOST_RUST_TYPE DEB_HOST_GNU_TYPE
+export CARGO_HOME = $(CURDIR)/debian/cargo_home
+
+export DEB_CARGO_CRATE=proxmox-network-interface-pinning_$(DEB_VERSION_UPSTREAM)
+export DEB_CARGO_PACKAGE=proxmox-network-interface-pinning
+
+%:
+ dh $@
+
+override_dh_auto_configure:
+ @perl -ne 'if (/^version\s*=\s*"(\d+(?:\.\d+)+)"/) { my $$v_cargo = $$1; my $$v_deb = "$(DEB_VERSION_UPSTREAM)"; \
+ die "ERROR: d/changelog <-> Cargo.toml version mismatch: $$v_cargo != $$v_deb\n" if $$v_cargo ne $$v_deb; exit(0); }' Cargo.toml
+ $(CARGO) prepare-debian $(CURDIR)/debian/cargo_registry --link-from-system
+ dh_auto_configure
+
+override_dh_missing:
+ dh_missing --fail-missing
+
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..33d9ce7
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,579 @@
+use std::collections::{HashMap, HashSet};
+use std::fmt::{Display, Formatter};
+use std::ops::{Deref, DerefMut};
+use std::os::unix::process::CommandExt;
+use std::process::Command;
+use std::process::Stdio;
+
+use anyhow::{anyhow, bail, format_err, Error};
+use nix::unistd::{Uid, User};
+use proxmox_network_api::IpLink;
+use serde::de::{value::MapDeserializer, Deserialize};
+
+use proxmox_log::{debug, LevelFilter};
+use proxmox_network_types::mac_address::MacAddress;
+use proxmox_router::cli::{
+ run_cli_command, CliCommand, CliCommandMap, CliEnvironment, Confirmation,
+};
+use proxmox_schema::api;
+use walkdir::WalkDir;
+
+const SYSTEMD_LINK_FILE_PATH: &str = "/usr/local/lib/systemd/network";
+
+#[derive(Debug, Clone, serde::Deserialize, Default)]
+pub struct InterfaceMapping {
+ mapping: HashMap<String, String>,
+}
+
+impl Deref for InterfaceMapping {
+ type Target = HashMap<String, String>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.mapping
+ }
+}
+
+impl DerefMut for InterfaceMapping {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.mapping
+ }
+}
+
+impl InterfaceMapping {
+ pub fn write(&self, ip_links: HashMap<String, IpLink>) -> Result<(), Error> {
+ if self.mapping.is_empty() {
+ return Ok(());
+ }
+
+ println!("Generating link files");
+
+ std::fs::create_dir_all(SYSTEMD_LINK_FILE_PATH)?;
+
+ let mut sorted_links: Vec<&IpLink> = ip_links.values().collect();
+ sorted_links.sort_by_key(|a| a.index());
+
+ for ip_link in sorted_links {
+ if let Some(new_name) = self.mapping.get(ip_link.name()) {
+ let link_file = LinkFile::new_ether(ip_link.permanent_mac(), new_name.to_string());
+
+ std::fs::write(
+ format!("{}/{}", SYSTEMD_LINK_FILE_PATH, link_file.file_name()),
+ link_file.to_string().as_bytes(),
+ )?;
+ }
+ }
+
+ println!("Successfully generated .link files in '/usr/local/lib/systemd/network/'");
+ Ok(())
+ }
+}
+
+struct PinnedInterfaces {
+ mapping: HashMap<MacAddress, String>,
+}
+
+impl PinnedInterfaces {
+ pub fn get(&self, mac_address: &MacAddress) -> Option<&String> {
+ self.mapping.get(mac_address)
+ }
+
+ pub fn values(&self) -> impl Iterator<Item = &String> {
+ self.mapping.values()
+ }
+
+ pub fn contains_key(&self, mac_address: &MacAddress) -> bool {
+ self.mapping.contains_key(mac_address)
+ }
+}
+
+impl FromIterator<LinkFile> for PinnedInterfaces {
+ fn from_iter<T: IntoIterator<Item = LinkFile>>(iter: T) -> Self {
+ Self {
+ mapping: iter
+ .into_iter()
+ .map(|link_file| {
+ (
+ link_file.match_section.mac_address,
+ link_file.link_section.name,
+ )
+ })
+ .collect(),
+ }
+ }
+}
+
+#[derive(Debug, Clone, serde::Deserialize)]
+struct MatchSection {
+ #[serde(rename = "MACAddress")]
+ mac_address: MacAddress,
+ #[serde(rename = "Type")]
+ ty: String,
+}
+
+impl Display for MatchSection {
+ fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
+ writeln!(f, "MACAddress={}", self.mac_address)?;
+ writeln!(f, "Type={}", self.ty)?;
+
+ Ok(())
+ }
+}
+
+#[derive(Debug, Clone, serde::Deserialize)]
+struct LinkSection {
+ #[serde(rename = "Name")]
+ name: String,
+}
+
+impl Display for LinkSection {
+ fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
+ writeln!(f, "Name={}", self.name)
+ }
+}
+
+#[derive(Debug, Clone, serde::Deserialize)]
+struct LinkFile {
+ match_section: MatchSection,
+ link_section: LinkSection,
+}
+
+impl LinkFile {
+ pub fn new_ether(mac_address: MacAddress, name: String) -> Self {
+ Self {
+ match_section: MatchSection {
+ mac_address,
+ ty: "ether".to_string(),
+ },
+ link_section: LinkSection { name },
+ }
+ }
+
+ pub fn file_name(&self) -> String {
+ format!("50-pve-{}.link", self.link_section.name)
+ }
+}
+
+#[derive(Debug, Clone, serde::Deserialize, Hash, Eq, PartialEq)]
+pub enum LinkFileSection {
+ Match,
+ Link,
+}
+
+impl std::str::FromStr for LinkFileSection {
+ type Err = Error;
+
+ fn from_str(value: &str) -> Result<Self, Self::Err> {
+ Ok(match value {
+ "[Match]" => LinkFileSection::Match,
+ "[Link]" => LinkFileSection::Link,
+ _ => bail!("invalid section type: {value}"),
+ })
+ }
+}
+
+impl Display for LinkFileSection {
+ fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
+ writeln!(
+ f,
+ "{}",
+ match self {
+ LinkFileSection::Link => "[Link]",
+ LinkFileSection::Match => "[Match]",
+ }
+ )
+ }
+}
+
+#[derive(Debug)]
+pub struct SerdeStringError(String);
+
+impl std::error::Error for SerdeStringError {}
+
+impl Display for SerdeStringError {
+ fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
+ f.write_str(&self.0)
+ }
+}
+
+impl serde::de::Error for SerdeStringError {
+ fn custom<T: Display>(msg: T) -> Self {
+ Self(msg.to_string())
+ }
+}
+
+impl std::str::FromStr for LinkFile {
+ type Err = Error;
+
+ fn from_str(value: &str) -> Result<Self, Self::Err> {
+ let mut sections: HashMap<LinkFileSection, HashMap<String, String>> = HashMap::new();
+ let mut current_section = None;
+
+ for line in value.lines() {
+ let line = line.trim();
+
+ if line.is_empty() || line.starts_with(['#', ';']) {
+ continue;
+ }
+
+ if line.starts_with('[') {
+ current_section = Some(line.parse()?);
+ sections.insert(current_section.as_ref().cloned().unwrap(), HashMap::new());
+ } else {
+ if current_section.is_none() {
+ bail!("config line without section")
+ }
+
+ let Some((key, value)) = line.split_once("=") else {
+ bail!("could not find key, value pair in link config");
+ };
+
+ if key.is_empty() || value.is_empty() {
+ bail!("could not find key, value pair in link config");
+ }
+
+ sections
+ .get_mut(current_section.as_ref().expect("current section is some"))
+ .expect("section has been inserted")
+ .insert(key.to_string(), value.to_string());
+ }
+ }
+
+ let link_data = sections
+ .remove(&LinkFileSection::Link)
+ .ok_or_else(|| anyhow!("no link section in link file"))?;
+
+ let link_section = LinkSection::deserialize(MapDeserializer::<
+ std::collections::hash_map::IntoIter<String, String>,
+ SerdeStringError,
+ >::new(link_data.into_iter()))?;
+
+ let match_data = sections
+ .remove(&LinkFileSection::Match)
+ .ok_or_else(|| anyhow!("no match section in link file"))?;
+
+ let match_section = MatchSection::deserialize(MapDeserializer::<
+ std::collections::hash_map::IntoIter<String, String>,
+ SerdeStringError,
+ >::new(match_data.into_iter()))?;
+
+ Ok(Self {
+ match_section,
+ link_section,
+ })
+ }
+}
+
+impl Display for LinkFile {
+ fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
+ write!(f, "{}", LinkFileSection::Match)?;
+ writeln!(f, "{}", self.match_section)?;
+
+ write!(f, "{}", LinkFileSection::Link)?;
+ writeln!(f, "{}", self.link_section)?;
+
+ Ok(())
+ }
+}
+
+pub struct PinningTool {
+ ip_links: HashMap<String, IpLink>,
+ pinned_interfaces: PinnedInterfaces,
+ existing_names: HashSet<String>,
+}
+
+impl PinningTool {
+ fn read_link_files() -> Result<Vec<LinkFile>, Error> {
+ debug!("reading link data");
+
+ let link_files = WalkDir::new(SYSTEMD_LINK_FILE_PATH)
+ .max_depth(1)
+ .into_iter()
+ .filter_map(|entry| entry.ok())
+ .filter(|entry| {
+ if let Some(file_name) = entry.path().file_name() {
+ let name = file_name.to_str().unwrap();
+ return name.starts_with("50-pve-") && name.ends_with(".link");
+ }
+
+ false
+ });
+
+ let mut ip_links = Vec::new();
+
+ for link_file in link_files {
+ debug!("reading file {}", link_file.path().display());
+
+ let file_content = std::fs::read(link_file.path())?;
+ let ip_link = std::str::from_utf8(&file_content)?;
+
+ ip_links.push(ip_link.parse()?);
+ }
+
+ Ok(ip_links)
+ }
+
+ pub fn new() -> Result<Self, Error> {
+ let ip_links = proxmox_network_api::get_network_interfaces()?;
+ let pinned_interfaces: PinnedInterfaces = Self::read_link_files()?.into_iter().collect();
+
+ let mut existing_names = HashSet::new();
+
+ for name in ip_links.keys() {
+ existing_names.insert(name.clone());
+ }
+
+ for pinned_interface in pinned_interfaces.values() {
+ existing_names.insert(pinned_interface.clone());
+ }
+
+ Ok(Self {
+ ip_links,
+ pinned_interfaces,
+ existing_names,
+ })
+ }
+
+ pub fn pin_interface(
+ self,
+ interface_name: &str,
+ target_name: Option<String>,
+ prefix: Option<String>,
+ ) -> Result<InterfaceMapping, Error> {
+ let ip_link = self
+ .ip_links
+ .get(interface_name)
+ .ok_or_else(|| anyhow!("cannot find interface with name {interface_name}"))?;
+
+ if self
+ .pinned_interfaces
+ .contains_key(&ip_link.permanent_mac())
+ {
+ bail!("pin already exists for interface {interface_name}");
+ }
+
+ let mut mapping = InterfaceMapping::default();
+
+ if let Some(target_name) = target_name {
+ if self.existing_names.contains(&target_name) {
+ bail!("target name already exists");
+ }
+
+ let mut current_altnames = Vec::new();
+
+ mapping.insert(ip_link.name().to_string(), target_name.to_string());
+
+ for altname in ip_link.altnames() {
+ current_altnames.push(altname.as_str());
+ mapping.insert(altname.to_string(), target_name.to_string());
+ }
+
+ println!(
+ "Name for {} ({}) will change to {target_name}",
+ ip_link.name(),
+ current_altnames.join(", ")
+ );
+ } else if let Some(prefix) = prefix {
+ let mut idx = 0;
+
+ loop {
+ let target_name = format!("{prefix}{idx}");
+
+ if !self.existing_names.contains(&target_name) {
+ let mut current_altnames = Vec::new();
+
+ mapping.insert(ip_link.name().to_string(), target_name.to_string());
+
+ for altname in ip_link.altnames() {
+ current_altnames.push(altname.as_str());
+ mapping.insert(altname.to_string(), target_name.to_string());
+ }
+
+ println!(
+ "Name for {} ({}) will change to {target_name}",
+ ip_link.name(),
+ current_altnames.join(", ")
+ );
+
+ break;
+ }
+
+ idx += 1;
+ }
+ } else {
+ return Err(anyhow!(
+ "neither target-name nor prefix provided for interface"
+ ));
+ }
+
+ mapping.write(self.ip_links)?;
+ Ok(mapping)
+ }
+
+ pub fn pin_all(mut self, prefix: &str) -> Result<InterfaceMapping, Error> {
+ let mut mapping = InterfaceMapping::default();
+
+ let mut idx = 0;
+
+ for (name, ip_link) in &self.ip_links {
+ if ip_link.is_physical()
+ && self
+ .pinned_interfaces
+ .get(&ip_link.permanent_mac())
+ .is_none()
+ {
+ loop {
+ let target_name = format!("{prefix}{idx}");
+
+ if !self.existing_names.contains(&target_name) {
+ let mut current_altnames = Vec::new();
+
+ mapping.insert(name.to_string(), target_name.to_string());
+
+ for altname in ip_link.altnames() {
+ current_altnames.push(altname.as_str());
+ mapping.insert(altname.to_string(), target_name.to_string());
+ }
+
+ println!(
+ "Name for {} ({}) will change to {target_name}",
+ ip_link.name(),
+ current_altnames.join(", ")
+ );
+
+ self.existing_names.insert(target_name);
+
+ break;
+ }
+
+ idx += 1;
+ }
+ }
+ }
+
+ mapping.write(self.ip_links)?;
+ Ok(mapping)
+ }
+}
+
+#[api(
+ input: {
+ properties: {
+ interface: {
+ type: String,
+ optional: true,
+ description: "Only pin a specific interface.",
+ },
+ prefix: {
+ type: String,
+ optional: true,
+ description: "Use a specific prefix for automatically choosing the pinned name.",
+ },
+ "target-name": {
+ type: String,
+ optional: true,
+ description: "Pin the interface to a specific name.",
+ },
+ }
+ }
+)]
+/// Generates link files to pin the names of network interfaces (based on MAC address).
+fn generate_mapping(
+ interface: Option<String>,
+ prefix: Option<String>,
+ target_name: Option<String>,
+) -> Result<(), Error> {
+ let pinning_tool = PinningTool::new()?;
+
+ let target = if let Some(ref interface) = interface {
+ interface.as_str()
+ } else {
+ "all interfaces"
+ };
+
+ let confirmation = Confirmation::query_with_default(
+ format!("This will generate name pinning configuration for {target} - continue (y/N)?")
+ .as_str(),
+ Confirmation::No,
+ )?;
+
+ if confirmation.is_no() {
+ return Ok(());
+ }
+
+ let mapping = if let Some(interface) = interface {
+ pinning_tool.pin_interface(&interface, target_name, prefix)?
+ } else {
+ let prefix = prefix.unwrap_or("nic".to_string());
+ pinning_tool.pin_all(&prefix)?
+ };
+
+ if mapping.is_empty() {
+ println!("Nothing to do. Aborting.");
+ return Ok(());
+ }
+
+ println!("Updating /etc/network/interfaces.new");
+
+ proxmox_network_api::lock_config()?;
+
+ let (mut config, _) = proxmox_network_api::config()?;
+ config.rename_interfaces(mapping.deref())?;
+
+ proxmox_network_api::save_config(&config)?;
+
+ println!("Successfully updated network configuration files.");
+
+ println!("\nPlease reboot to apply the changes to your configuration\n");
+
+ Ok(())
+}
+
+// TODO: make this load the unprivileged user dynamically, depending on product, default to backup
+// for now since we only ship the tool with PBS currently
+pub fn unprivileged_user() -> Result<nix::unistd::User, Error> {
+ if cfg!(test) {
+ Ok(User::from_uid(Uid::current())?.expect("current user does not exist"))
+ } else {
+ User::from_name("backup")?.ok_or_else(|| format_err!("Unable to lookup 'backup' user."))
+ }
+}
+
+pub fn privileged_user() -> Result<nix::unistd::User, Error> {
+ if cfg!(test) {
+ Ok(User::from_uid(Uid::current())?.expect("current user does not exist"))
+ } else {
+ User::from_name("root")?.ok_or_else(|| format_err!("Unable to lookup superuser."))
+ }
+}
+
+fn main() -> Result<(), Error> {
+ // This is run on a PVE host, so we use the PVE-specific pinning tool instead with the
+ // parameters supplied.
+ if std::fs::exists("/usr/bin/proxmox-network-interface-pinning")? {
+ let args = std::env::args().skip(1);
+
+ return Err(Command::new("/usr/bin/proxmox-network-interface-pinning")
+ .args(args)
+ .stdout(Stdio::inherit())
+ .stderr(Stdio::inherit())
+ .exec()
+ .into());
+ }
+
+ proxmox_log::Logger::from_env("PBS_LOG", LevelFilter::INFO)
+ .stderr()
+ .init()
+ .expect("failed to initiate logger");
+
+ // required for locking the network config
+ proxmox_product_config::init(unprivileged_user()?, privileged_user()?);
+
+ let generate_command = CliCommand::new(&API_METHOD_GENERATE_MAPPING);
+ let commands = CliCommandMap::new().insert("generate", generate_command);
+
+ let rpcenv = CliEnvironment::new();
+
+ run_cli_command(commands, rpcenv, None);
+
+ Ok(())
+}
--
2.47.2
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [pbs-devel] [PATCH proxmox-network-interface-pinning 1/1] initial commit
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox-network-interface-pinning 1/1] initial commit Stefan Hanreich
@ 2025-07-30 13:07 ` Thomas Lamprecht
2025-07-30 13:14 ` Stefan Hanreich
0 siblings, 1 reply; 16+ messages in thread
From: Thomas Lamprecht @ 2025-07-30 13:07 UTC (permalink / raw)
To: Proxmox Backup Server development discussion, Stefan Hanreich
Am 29.07.25 um 18:57 schrieb Stefan Hanreich:
> + // This is run on a PVE host, so we use the PVE-specific pinning tool instead with the
> + // parameters supplied.
> + if std::fs::exists("/usr/bin/proxmox-network-interface-pinning")? {
Hmm, why does this here live in libexec if it's intended to be the main one?
Should we rather move the one from pve-manager into libexec with a product
specific name like "pve-network-interface-pinning" and keep this here in
bin with the generic name? As otherwise one needs to use the full libexec
path when using this on PBS/PMG/PDM? Or what's the idea here?
btw. no need to commit Cargo.lock here, at least not for initial commit
to the list.
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [pbs-devel] [PATCH proxmox-network-interface-pinning 1/1] initial commit
2025-07-30 13:07 ` Thomas Lamprecht
@ 2025-07-30 13:14 ` Stefan Hanreich
2025-07-30 13:24 ` Thomas Lamprecht
0 siblings, 1 reply; 16+ messages in thread
From: Stefan Hanreich @ 2025-07-30 13:14 UTC (permalink / raw)
To: Thomas Lamprecht, Proxmox Backup Server development discussion
On 7/30/25 3:07 PM, Thomas Lamprecht wrote:
> Am 29.07.25 um 18:57 schrieb Stefan Hanreich:
>> + // This is run on a PVE host, so we use the PVE-specific pinning tool instead with the
>> + // parameters supplied.
>> + if std::fs::exists("/usr/bin/proxmox-network-interface-pinning")? {
>
> Hmm, why does this here live in libexec if it's intended to be the main one?
>
> Should we rather move the one from pve-manager into libexec with a product
> specific name like "pve-network-interface-pinning" and keep this here in
> bin with the generic name? As otherwise one needs to use the full libexec
> path when using this on PBS/PMG/PDM? Or what's the idea here?
Yes, that sounds better, so the pve-manager one into
/usr/libexec/proxmox/pve-network-interface-pinning
and this one into
/usr/bin/proxmox-network-interface-pinning
Or even sbin?
I assume, we would then install the standalone package by default in PVE?
> btw. no need to commit Cargo.lock here, at least not for initial commit
> to the list.
duly noted
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [pbs-devel] [PATCH proxmox-network-interface-pinning 1/1] initial commit
2025-07-30 13:14 ` Stefan Hanreich
@ 2025-07-30 13:24 ` Thomas Lamprecht
2025-07-30 13:30 ` Fabian Grünbichler
0 siblings, 1 reply; 16+ messages in thread
From: Thomas Lamprecht @ 2025-07-30 13:24 UTC (permalink / raw)
To: Proxmox Backup Server development discussion, Stefan Hanreich
Am 30.07.25 um 15:14 schrieb Stefan Hanreich:
> On 7/30/25 3:07 PM, Thomas Lamprecht wrote:
>> Am 29.07.25 um 18:57 schrieb Stefan Hanreich:
>>> + // This is run on a PVE host, so we use the PVE-specific pinning tool instead with the
>>> + // parameters supplied.
>>> + if std::fs::exists("/usr/bin/proxmox-network-interface-pinning")? {
>>
>> Hmm, why does this here live in libexec if it's intended to be the main one?
>>
>> Should we rather move the one from pve-manager into libexec with a product
>> specific name like "pve-network-interface-pinning" and keep this here in
>> bin with the generic name? As otherwise one needs to use the full libexec
>> path when using this on PBS/PMG/PDM? Or what's the idea here?
>
> Yes, that sounds better, so the pve-manager one into
>
> /usr/libexec/proxmox/pve-network-interface-pinning
>
> and this one into
>
> /usr/bin/proxmox-network-interface-pinning
>
> Or even sbin?
bin an sbin will be probably merged in a future major release anyway as
systemd pushes for doing so, so that doesn't really matters.
>
> I assume, we would then install the standalone package by default in PVE?
That's the only small "ugliness" there is with this approach, as it would
not be required per se.
The alternative I see is that both life in /usr/bin, either with
"pve" and "proxmox" prefix in the name, respectively, or under the same
name but with conflicts on packaging level and this package here being
added only as Recommended for PBS/PDM/PMG to allow co-installation.
tbh. I'm not opposed of either variant, CC'in Fabian, maybe he can act
as tie breaker here.
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [pbs-devel] [PATCH proxmox-network-interface-pinning 1/1] initial commit
2025-07-30 13:24 ` Thomas Lamprecht
@ 2025-07-30 13:30 ` Fabian Grünbichler
2025-07-30 13:35 ` Stefan Hanreich
0 siblings, 1 reply; 16+ messages in thread
From: Fabian Grünbichler @ 2025-07-30 13:30 UTC (permalink / raw)
To: Proxmox Backup Server development discussion, Stefan Hanreich,
Thomas Lamprecht
On July 30, 2025 3:24 pm, Thomas Lamprecht wrote:
> Am 30.07.25 um 15:14 schrieb Stefan Hanreich:
>> On 7/30/25 3:07 PM, Thomas Lamprecht wrote:
>>> Am 29.07.25 um 18:57 schrieb Stefan Hanreich:
>>>> + // This is run on a PVE host, so we use the PVE-specific pinning tool instead with the
>>>> + // parameters supplied.
>>>> + if std::fs::exists("/usr/bin/proxmox-network-interface-pinning")? {
>>>
>>> Hmm, why does this here live in libexec if it's intended to be the main one?
>>>
>>> Should we rather move the one from pve-manager into libexec with a product
>>> specific name like "pve-network-interface-pinning" and keep this here in
>>> bin with the generic name? As otherwise one needs to use the full libexec
>>> path when using this on PBS/PMG/PDM? Or what's the idea here?
>>
>> Yes, that sounds better, so the pve-manager one into
>>
>> /usr/libexec/proxmox/pve-network-interface-pinning
>>
>> and this one into
>>
>> /usr/bin/proxmox-network-interface-pinning
>>
>> Or even sbin?
>
> bin an sbin will be probably merged in a future major release anyway as
> systemd pushes for doing so, so that doesn't really matters.
>
>>
>> I assume, we would then install the standalone package by default in PVE?
>
> That's the only small "ugliness" there is with this approach, as it would
> not be required per se.
>
> The alternative I see is that both life in /usr/bin, either with
> "pve" and "proxmox" prefix in the name, respectively, or under the same
> name but with conflicts on packaging level and this package here being
> added only as Recommended for PBS/PDM/PMG to allow co-installation.
>
> tbh. I'm not opposed of either variant, CC'in Fabian, maybe he can act
> as tie breaker here.
I think installing the rust one always is the simplest solution, even if
it is basically a wrapper doing nothing except forwarding the invocation
on systems with PVE installed.
we could also solve this cleanly on the packaging level, but all the
variants I can come up with[0] are more involved - and at some point we
will probably want to drop the PVE specific one anyway, i.e., when we
switch the network config parsing there over to the Rust-based one one
it reaches feature parity ;)
0: alternatives preferring the PVE one, diverting by the PVE package, ..
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [pbs-devel] [PATCH proxmox-network-interface-pinning 1/1] initial commit
2025-07-30 13:30 ` Fabian Grünbichler
@ 2025-07-30 13:35 ` Stefan Hanreich
2025-07-30 13:42 ` Thomas Lamprecht
0 siblings, 1 reply; 16+ messages in thread
From: Stefan Hanreich @ 2025-07-30 13:35 UTC (permalink / raw)
To: Fabian Grünbichler,
Proxmox Backup Server development discussion, Thomas Lamprecht
On 7/30/25 3:30 PM, Fabian Grünbichler wrote:
[snip]
>>> I assume, we would then install the standalone package by default in PVE?
>>
>> That's the only small "ugliness" there is with this approach, as it would
>> not be required per se.
>>
>> The alternative I see is that both life in /usr/bin, either with
>> "pve" and "proxmox" prefix in the name, respectively, or under the same
>> name but with conflicts on packaging level and this package here being
>> added only as Recommended for PBS/PDM/PMG to allow co-installation.
>>
>> tbh. I'm not opposed of either variant, CC'in Fabian, maybe he can act
>> as tie breaker here.
>
> I think installing the rust one always is the simplest solution, even if
> it is basically a wrapper doing nothing except forwarding the invocation
> on systems with PVE installed.
>
> we could also solve this cleanly on the packaging level, but all the
> variants I can come up with[0] are more involved - and at some point we
> will probably want to drop the PVE specific one anyway, i.e., when we
> switch the network config parsing there over to the Rust-based one one
> it reaches feature parity ;)
>
> 0: alternatives preferring the PVE one, diverting by the PVE package, ..
Yeh, I'm also gravitating towards that solution, imo the easiest path
after all.
So:
pve-network-interface-pinning in /usr/libexec/proxmox
proxmox-network-interface-pinning in /usr/bin
I'll send a v2 for this series, as well as a new patch for pve-manager -
unless there are any objections? @Thomas
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
^ permalink raw reply [flat|nested] 16+ messages in thread
* Re: [pbs-devel] [PATCH proxmox-network-interface-pinning 1/1] initial commit
2025-07-30 13:35 ` Stefan Hanreich
@ 2025-07-30 13:42 ` Thomas Lamprecht
0 siblings, 0 replies; 16+ messages in thread
From: Thomas Lamprecht @ 2025-07-30 13:42 UTC (permalink / raw)
To: Stefan Hanreich, Fabian Grünbichler,
Proxmox Backup Server development discussion
>> I think installing the rust one always is the simplest solution, even if
>> it is basically a wrapper doing nothing except forwarding the invocation
>> on systems with PVE installed.
ack, thanks for your input @Fabian
Am 30.07.25 um 15:35 schrieb Stefan Hanreich:
> pve-network-interface-pinning in /usr/libexec/proxmox
> proxmox-network-interface-pinning in /usr/bin
>
> I'll send a v2 for this series, as well as a new patch for pve-manager -
> unless there are any objections? @Thomas
>
No objections from my side, above sounds good to me.
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
^ permalink raw reply [flat|nested] 16+ messages in thread
* [pbs-devel] superseded: [PATCH proxmox{-ve-rs, , -backup, -firewall, -network-interface-pinning} 0/8] proxmox-network-interface-pinning
2025-07-29 16:56 [pbs-devel] [PATCH proxmox{-ve-rs, , -backup, -firewall, -network-interface-pinning} 0/8] proxmox-network-interface-pinning Stefan Hanreich
` (7 preceding siblings ...)
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox-network-interface-pinning 1/1] initial commit Stefan Hanreich
@ 2025-07-30 14:37 ` Stefan Hanreich
8 siblings, 0 replies; 16+ messages in thread
From: Stefan Hanreich @ 2025-07-30 14:37 UTC (permalink / raw)
To: pbs-devel
https://lore.proxmox.com/pbs-devel/20250730141550.281340-1-s.hanreich@proxmox.com/T/#t
On 7/29/25 6:56 PM, Stefan Hanreich wrote:
> Introduce the proxmox-network-interface-pinning tool for PBS, written in Rust.
>
> I basically had to do the same changes to the network stack in PBS, that I
> already had to do for Proxmox VE:
>
> * use ip link for determining physical interfaces in the network configuration
> stack
> * move metric collection over to this new method as well
>
> In the process I moved the existing functions for querying 'ip link', that were
> already used by the firewall, to proxmox-network-api which seems like a better
> fit than proxmox-ve-config (which was only a temporary solution anyway).
>
> I also decided to move PBS over to the implementations contained in
> proxmox-network-api, but if this is considered too drastic a change so close to
> a release (I'm torn myself), then it should be trivial to revert those changes
> and implement / copy-paste everything to the respective projects instead.
>
> PBS does not automatically apply pending changes on reboot. It is already agreed
> with @Thomas, that he will add a respective service to the PBS repository.
>
> proxmox-backup depends on proxmox-network-api
> proxmox-firewall depends on proxmox-network-api
> proxmox-network-interface-pinning depends on proxmox-network-api
>
> proxmox-ve-rs:
>
> Stefan Hanreich (1):
> host: network: move to proxmox-network-api
>
> proxmox-ve-config/src/host/mod.rs | 1 -
> proxmox-ve-config/src/host/network.rs | 35 ---------------------------
> 2 files changed, 36 deletions(-)
> delete mode 100644 proxmox-ve-config/src/host/network.rs
>
>
> proxmox:
>
> Stefan Hanreich (3):
> pbs-api-types: use proxmox-network-api types
> proxmox-network-api: use ip link for querying interface information
> network-api: add rename_interfaces method
>
> Cargo.toml | 1 +
> pbs-api-types/src/network.rs | 345 -----------------------
> proxmox-network-api/Cargo.toml | 2 +
> proxmox-network-api/debian/control | 8 +-
> proxmox-network-api/src/api_types.rs | 8 +-
> proxmox-network-api/src/config/helper.rs | 158 +++++++----
> proxmox-network-api/src/config/mod.rs | 72 ++++-
> proxmox-network-api/src/config/parser.rs | 37 ++-
> 8 files changed, 206 insertions(+), 425 deletions(-)
> delete mode 100644 pbs-api-types/src/network.rs
>
>
> proxmox-backup:
>
> Stefan Hanreich (2):
> config: network: move to proxmox-network-api
> metric_collection: use ip link for determining the type of interfaces
>
> Cargo.toml | 5 +
> debian/control | 1 +
> pbs-config/src/lib.rs | 10 +-
> pbs-config/src/network/helper.rs | 223 -----
> pbs-config/src/network/lexer.rs | 136 ---
> pbs-config/src/network/mod.rs | 687 ---------------
> pbs-config/src/network/parser.rs | 846 -------------------
> src/api2/node/network.rs | 13 +-
> src/bin/proxmox-backup-api.rs | 3 +-
> src/bin/proxmox-backup-manager.rs | 1 +
> src/bin/proxmox-backup-proxy.rs | 1 +
> src/bin/proxmox_backup_manager/network.rs | 14 +-
> src/server/metric_collection/mod.rs | 87 +-
> src/server/metric_collection/pull_metrics.rs | 5 +-
> src/server/metric_collection/rrd.rs | 5 +-
> 15 files changed, 112 insertions(+), 1925 deletions(-)
> delete mode 100644 pbs-config/src/network/helper.rs
> delete mode 100644 pbs-config/src/network/lexer.rs
> delete mode 100644 pbs-config/src/network/mod.rs
> delete mode 100644 pbs-config/src/network/parser.rs
>
>
> proxmox-firewall:
>
> Stefan Hanreich (1):
> firewall: config: use proxmox-network-api
>
> proxmox-firewall/Cargo.toml | 3 ++-
> proxmox-firewall/src/config.rs | 29 +++++----------------
> proxmox-firewall/tests/integration_tests.rs | 8 +++---
> 3 files changed, 12 insertions(+), 28 deletions(-)
>
>
> proxmox-network-interface-pinning:
>
> Stefan Hanreich (1):
> initial commit
>
>
> Summary over all repositories:
> 28 files changed, 330 insertions(+), 2414 deletions(-)
>
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
^ permalink raw reply [flat|nested] 16+ messages in thread
end of thread, other threads:[~2025-07-30 14:36 UTC | newest]
Thread overview: 16+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-07-29 16:56 [pbs-devel] [PATCH proxmox{-ve-rs, , -backup, -firewall, -network-interface-pinning} 0/8] proxmox-network-interface-pinning Stefan Hanreich
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox-ve-rs 1/1] host: network: move to proxmox-network-api Stefan Hanreich
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox 1/3] pbs-api-types: use proxmox-network-api types Stefan Hanreich
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox 2/3] proxmox-network-api: use ip link for querying interface information Stefan Hanreich
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox 3/3] network-api: add rename_interfaces method Stefan Hanreich
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox-backup 1/2] config: network: move to proxmox-network-api Stefan Hanreich
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox-backup 2/2] metric_collection: use ip link for determining the type of interfaces Stefan Hanreich
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox-firewall 1/1] firewall: config: use proxmox-network-api Stefan Hanreich
2025-07-29 16:56 ` [pbs-devel] [PATCH proxmox-network-interface-pinning 1/1] initial commit Stefan Hanreich
2025-07-30 13:07 ` Thomas Lamprecht
2025-07-30 13:14 ` Stefan Hanreich
2025-07-30 13:24 ` Thomas Lamprecht
2025-07-30 13:30 ` Fabian Grünbichler
2025-07-30 13:35 ` Stefan Hanreich
2025-07-30 13:42 ` Thomas Lamprecht
2025-07-30 14:37 ` [pbs-devel] superseded: [PATCH proxmox{-ve-rs, , -backup, -firewall, -network-interface-pinning} 0/8] proxmox-network-interface-pinning Stefan Hanreich
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.