[pbs-devel] [PATCH proxmox-backup v2 1/2] config: network: move to proxmox-network-api

Stefan Hanreich s.hanreich at proxmox.com
Wed Jul 30 16:15:45 CEST 2025


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 at 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




More information about the pbs-devel mailing list