remove system-management-api

Which is now split into separate crates:

- time-api
- network-api
- dns-api
- syslog-api
This commit is contained in:
Dietmar Maurer 2024-05-30 09:53:59 +02:00
parent f6bcb6b50b
commit 40f812f324
22 changed files with 0 additions and 3425 deletions

View File

@ -34,7 +34,6 @@ members = [
"proxmox-subscription",
"proxmox-sys",
"proxmox-syslog-api",
"proxmox-system-management-api",
"proxmox-tfa",
"proxmox-time",
"proxmox-time-api",

View File

@ -1,59 +0,0 @@
[package]
name = "proxmox-system-management-api"
version = "0.1.0"
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
exclude.workspace = true
description = "System Management API implementation (DNS, Time, Network)"
[dependencies]
anyhow.workspace = true
const_format.workspace = true
lazy_static.workspace = true
regex.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
nix = { workspace = true, optional = true }
libc = { workspace = true, optional = true }
log = { workspace = true, optional = true }
proxmox-sys = { workspace = true, optional = true }
proxmox-schema = { workspace = true, features = ["api-macro", "api-types"] }
proxmox-time = { workspace = true, optional = true }
proxmox-config-digest = { workspace = true, optional = true }
proxmox-product-config = { workspace = true, optional = true }
[features]
default = []
dns-api-types = ["dep:proxmox-config-digest"]
dns-impl = [
"dns-api-types",
"dep:proxmox-config-digest",
"proxmox-config-digest?/openssl",
"dep:proxmox-sys",
"dep:proxmox-time",
]
time-api-types = []
time-impl = [
"time-api-types",
"dep:proxmox-config-digest",
"proxmox-config-digest?/openssl",
"dep:proxmox-product-config",
"dep:proxmox-sys",
"dep:proxmox-time",
]
network-api-types = []
network-impl = [
"network-api-types",
"dep:proxmox-config-digest",
"proxmox-config-digest?/openssl",
"dep:proxmox-product-config",
"dep:nix",
"dep:libc",
"dep:proxmox-sys",
]
syslog-api-types = []
syslog-impl = ["syslog-api-types", "dep:log"]

View File

@ -1,6 +0,0 @@
rust-proxmox-system-management-api (0.1.0-1) bookworm; urgency=medium
* initial packaging
-- Proxmox Support Team <support@proxmox.com> Fri, 03 May 2024 09:06:35 +0200

View File

@ -1,18 +0,0 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Files:
*
Copyright: 2019 - 2023 Proxmox Server Solutions GmbH <support@proxmox.com>
License: AGPL-3.0-or-later
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 <https://www.gnu.org/licenses/>.

View File

@ -1,7 +0,0 @@
overlay = "."
crate_src_path = ".."
maintainer = "Proxmox Support Team <support@proxmox.com>"
[source]
vcs_git = "git://git.proxmox.com/git/proxmox.git"
vcs_browser = "https://git.proxmox.com/?p=proxmox.git"

View File

@ -1,94 +0,0 @@
use serde::{Deserialize, Serialize};
use proxmox_schema::api;
use proxmox_schema::api_types::IP_FORMAT;
use proxmox_schema::Schema;
use proxmox_schema::StringSchema;
use proxmox_config_digest::ConfigDigest;
pub const SEARCH_DOMAIN_SCHEMA: Schema =
StringSchema::new("Search domain for host-name lookup.").schema();
pub const FIRST_DNS_SERVER_SCHEMA: Schema = StringSchema::new("First name server IP address.")
.format(&IP_FORMAT)
.schema();
pub const SECOND_DNS_SERVER_SCHEMA: Schema = StringSchema::new("Second name server IP address.")
.format(&IP_FORMAT)
.schema();
pub const THIRD_DNS_SERVER_SCHEMA: Schema = StringSchema::new("Third name server IP address.")
.format(&IP_FORMAT)
.schema();
#[api(
properties: {
search: {
schema: SEARCH_DOMAIN_SCHEMA,
optional: true,
},
dns1: {
optional: true,
schema: FIRST_DNS_SERVER_SCHEMA,
},
dns2: {
optional: true,
schema: SECOND_DNS_SERVER_SCHEMA,
},
dns3: {
optional: true,
schema: THIRD_DNS_SERVER_SCHEMA,
},
options: {
description: "Other data found in the configuration file (resolv.conf).",
optional: true,
},
}
)]
#[derive(Serialize, Deserialize, Default)]
/// DNS configuration from '/etc/resolv.conf'
pub struct ResolvConf {
#[serde(skip_serializing_if = "Option::is_none")]
pub search: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dns1: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dns2: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dns3: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<String>,
}
#[api(
properties: {
config: {
type: ResolvConf,
},
digest: {
type: ConfigDigest,
},
}
)]
#[derive(Serialize, Deserialize)]
/// DNS configuration with digest.
pub struct ResolvConfWithDigest {
#[serde(flatten)]
pub config: ResolvConf,
pub digest: ConfigDigest,
}
#[api()]
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
/// Deletable DNS configuration property name
pub enum DeletableResolvConfProperty {
/// Delete first nameserver entry
Dns1,
/// Delete second nameserver entry
Dns2,
/// Delete third nameserver entry
Dns3,
}

View File

@ -1,7 +0,0 @@
mod api_types;
pub use api_types::*;
#[cfg(feature = "dns-impl")]
mod resolv_conf;
#[cfg(feature = "dns-impl")]
pub use resolv_conf::*;

View File

@ -1,144 +0,0 @@
use std::sync::Arc;
use std::sync::Mutex;
use anyhow::Error;
use const_format::concatcp;
use lazy_static::lazy_static;
use proxmox_config_digest::ConfigDigest;
use regex::Regex;
use proxmox_sys::fs::file_get_contents;
use proxmox_sys::fs::replace_file;
use proxmox_sys::fs::CreateOptions;
use proxmox_schema::api_types::IPRE_STR;
use super::DeletableResolvConfProperty;
use super::ResolvConf;
use super::ResolvConfWithDigest;
static RESOLV_CONF_FN: &str = "/etc/resolv.conf";
/// Read DNS configuration from '/etc/resolv.conf'.
pub fn read_etc_resolv_conf(
expected_digest: Option<&ConfigDigest>,
) -> Result<ResolvConfWithDigest, Error> {
let mut config = ResolvConf::default();
let mut nscount = 0;
let raw = file_get_contents(RESOLV_CONF_FN)?;
let digest = ConfigDigest::from_slice(&raw);
digest.detect_modification(expected_digest)?;
let data = String::from_utf8(raw)?;
lazy_static! {
static ref DOMAIN_REGEX: Regex = Regex::new(r"^\s*(?:search|domain)\s+(\S+)\s*").unwrap();
static ref SERVER_REGEX: Regex =
Regex::new(concatcp!(r"^\s*nameserver\s+(", IPRE_STR, r")\s*")).unwrap();
}
let mut options = String::new();
for line in data.lines() {
if let Some(caps) = DOMAIN_REGEX.captures(line) {
config.search = Some(caps[1].to_owned());
} else if let Some(caps) = SERVER_REGEX.captures(line) {
nscount += 1;
if nscount > 3 {
continue;
};
let nameserver = Some(caps[1].to_owned());
match nscount {
1 => config.dns1 = nameserver,
2 => config.dns2 = nameserver,
3 => config.dns3 = nameserver,
_ => continue,
}
} else {
if !options.is_empty() {
options.push('\n');
}
options.push_str(line);
}
}
if !options.is_empty() {
config.options = Some(options);
}
Ok(ResolvConfWithDigest { config, digest })
}
/// Update DNS configuration, write result back to '/etc/resolv.conf'.
pub fn update_dns(
update: ResolvConf,
delete: Option<Vec<DeletableResolvConfProperty>>,
digest: Option<ConfigDigest>,
) -> Result<(), Error> {
lazy_static! {
static ref MUTEX: Arc<Mutex<()>> = Arc::new(Mutex::new(()));
}
let _guard = MUTEX.lock();
let ResolvConfWithDigest { mut config, .. } = read_etc_resolv_conf(digest.as_ref())?;
if let Some(delete) = delete {
for delete_prop in delete {
match delete_prop {
DeletableResolvConfProperty::Dns1 => {
config.dns1 = None;
}
DeletableResolvConfProperty::Dns2 => {
config.dns2 = None;
}
DeletableResolvConfProperty::Dns3 => {
config.dns3 = None;
}
}
}
}
if update.search.is_some() {
config.search = update.search;
}
if update.dns1.is_some() {
config.dns1 = update.dns1;
}
if update.dns2.is_some() {
config.dns2 = update.dns2;
}
if update.dns3.is_some() {
config.dns3 = update.dns3;
}
let mut data = String::new();
use std::fmt::Write as _;
if let Some(search) = config.search {
let _ = writeln!(data, "search {}", search);
}
if let Some(dns1) = config.dns1 {
let _ = writeln!(data, "nameserver {}", dns1);
}
if let Some(dns2) = config.dns2 {
let _ = writeln!(data, "nameserver {}", dns2);
}
if let Some(dns3) = config.dns3 {
let _ = writeln!(data, "nameserver {}", dns3);
}
if let Some(options) = config.options {
data.push_str(&options);
}
replace_file(RESOLV_CONF_FN, data.as_bytes(), CreateOptions::new(), true)?;
Ok(())
}

View File

@ -1,11 +0,0 @@
#[cfg(feature = "dns-api-types")]
pub mod dns;
#[cfg(feature = "network-api-types")]
pub mod network;
#[cfg(feature = "time-api-types")]
pub mod time;
#[cfg(feature = "syslog-api-types")]
pub mod syslog;

View File

@ -1,347 +0,0 @@
use anyhow::{bail, Error};
use proxmox_config_digest::ConfigDigest;
use crate::network::{self, parse_vlan_id_from_name, parse_vlan_raw_device_from_name};
use crate::network::{
DeletableInterfaceProperty, Interface, InterfaceUpdater, LinuxBondMode, NetworkConfigMethod,
NetworkInterfaceType,
};
/// Create network interface configuration.
pub fn create_interface(iface: String, config: InterfaceUpdater) -> Result<(), Error> {
let interface_type = match config.interface_type {
Some(interface_type) => interface_type,
None => bail!("missing parameter 'type'"),
};
let _lock = network::lock_config()?;
let (mut network_config, _digest) = network::config()?;
if network_config.interfaces.contains_key(&iface) {
bail!("interface '{}' already exists", iface);
}
let mut interface = Interface::new(iface.clone());
interface.interface_type = interface_type;
if let Some(autostart) = config.autostart {
interface.autostart = autostart;
}
if config.method.is_some() {
interface.method = config.method;
}
if config.method6.is_some() {
interface.method6 = config.method6;
}
if config.mtu.is_some() {
interface.mtu = config.mtu;
}
if config.comments.is_some() {
interface.comments = config.comments;
}
if config.comments6.is_some() {
interface.comments6 = config.comments6;
}
if let Some(cidr) = config.cidr {
let (_, _, is_v6) = network::parse_cidr(&cidr)?;
if is_v6 {
bail!("invalid address type (expected IPv4, got IPv6)");
}
interface.cidr = Some(cidr);
}
if let Some(cidr6) = config.cidr6 {
let (_, _, is_v6) = network::parse_cidr(&cidr6)?;
if !is_v6 {
bail!("invalid address type (expected IPv6, got IPv4)");
}
interface.cidr6 = Some(cidr6);
}
if let Some(gateway) = config.gateway {
let is_v6 = gateway.contains(':');
if is_v6 {
bail!("invalid address type (expected IPv4, got IPv6)");
}
network_config.check_duplicate_gateway_v4(&iface)?;
interface.gateway = Some(gateway);
}
if let Some(gateway6) = config.gateway6 {
let is_v6 = gateway6.contains(':');
if !is_v6 {
bail!("invalid address type (expected IPv6, got IPv4)");
}
network_config.check_duplicate_gateway_v6(&iface)?;
interface.gateway6 = Some(gateway6);
}
match interface_type {
NetworkInterfaceType::Bridge => {
if let Some(ports) = &config.bridge_ports {
interface.set_bridge_port_list(ports)?;
}
if config.bridge_vlan_aware.is_some() {
interface.bridge_vlan_aware = config.bridge_vlan_aware;
}
}
NetworkInterfaceType::Bond => {
if let Some(mode) = config.bond_mode {
interface.bond_mode = config.bond_mode;
if config.bond_primary.is_some() {
if mode != LinuxBondMode::ActiveBackup {
bail!("bond-primary is only valid with Active/Backup mode");
}
interface.bond_primary = config.bond_primary;
}
if config.bond_xmit_hash_policy.is_some() {
if mode != LinuxBondMode::Ieee802_3ad && mode != LinuxBondMode::BalanceXor {
bail!("bond_xmit_hash_policy is only valid with LACP(802.3ad) or balance-xor mode");
}
interface.bond_xmit_hash_policy = config.bond_xmit_hash_policy;
}
}
if let Some(slaves) = &config.slaves {
interface.set_bond_slave_list(&slaves)?;
}
}
NetworkInterfaceType::Vlan => {
if config.vlan_id.is_none() && parse_vlan_id_from_name(&iface).is_none() {
bail!("vlan-id must be set");
}
interface.vlan_id = config.vlan_id;
if let Some(dev) = config
.vlan_raw_device
.as_deref()
.or_else(|| parse_vlan_raw_device_from_name(&iface))
{
if !network_config.interfaces.contains_key(dev) {
bail!("vlan-raw-device {dev} does not exist");
}
} else {
bail!("vlan-raw-device must be set");
}
interface.vlan_raw_device = config.vlan_raw_device;
}
_ => bail!(
"creating network interface type '{:?}' is not supported",
interface_type
),
}
if interface.cidr.is_some() || interface.gateway.is_some() {
interface.method = Some(NetworkConfigMethod::Static);
} else if interface.method.is_none() {
interface.method = Some(NetworkConfigMethod::Manual);
}
if interface.cidr6.is_some() || interface.gateway6.is_some() {
interface.method6 = Some(NetworkConfigMethod::Static);
} else if interface.method6.is_none() {
interface.method6 = Some(NetworkConfigMethod::Manual);
}
network_config.interfaces.insert(iface, interface);
network::save_config(&network_config)?;
Ok(())
}
/// Update network interface config.
pub fn update_interface(
iface: String,
update: InterfaceUpdater,
delete: Option<Vec<DeletableInterfaceProperty>>,
digest: Option<ConfigDigest>,
) -> Result<(), Error> {
let _lock = network::lock_config()?;
let (mut network_config, expected_digest) = network::config()?;
expected_digest.detect_modification(digest.as_ref())?;
if update.gateway.is_some() {
network_config.check_duplicate_gateway_v4(&iface)?;
}
if update.gateway6.is_some() {
network_config.check_duplicate_gateway_v6(&iface)?;
}
if let Some(dev) = update
.vlan_raw_device
.as_deref()
.or_else(|| parse_vlan_raw_device_from_name(&iface))
{
if !network_config.interfaces.contains_key(dev) {
bail!("vlan-raw-device {dev} does not exist");
}
}
let interface = network_config.lookup_mut(&iface)?;
if let Some(interface_type) = update.interface_type {
if interface_type != interface.interface_type {
bail!(
"got unexpected interface type ({:?} != {:?})",
interface_type,
interface.interface_type
);
}
}
if let Some(delete) = delete {
for delete_prop in delete {
match delete_prop {
DeletableInterfaceProperty::Cidr => {
interface.cidr = None;
}
DeletableInterfaceProperty::Cidr6 => {
interface.cidr6 = None;
}
DeletableInterfaceProperty::Gateway => {
interface.gateway = None;
}
DeletableInterfaceProperty::Gateway6 => {
interface.gateway6 = None;
}
DeletableInterfaceProperty::Method => {
interface.method = None;
}
DeletableInterfaceProperty::Method6 => {
interface.method6 = None;
}
DeletableInterfaceProperty::Comments => {
interface.comments = None;
}
DeletableInterfaceProperty::Comments6 => {
interface.comments6 = None;
}
DeletableInterfaceProperty::Mtu => {
interface.mtu = None;
}
DeletableInterfaceProperty::Autostart => {
interface.autostart = false;
}
DeletableInterfaceProperty::BridgePorts => {
interface.set_bridge_ports(Vec::new())?;
}
DeletableInterfaceProperty::BridgeVlanAware => {
interface.bridge_vlan_aware = None;
}
DeletableInterfaceProperty::Slaves => {
interface.set_bond_slaves(Vec::new())?;
}
DeletableInterfaceProperty::BondPrimary => {
interface.bond_primary = None;
}
DeletableInterfaceProperty::BondXmitHashPolicy => {
interface.bond_xmit_hash_policy = None
}
}
}
}
if let Some(autostart) = update.autostart {
interface.autostart = autostart;
}
if update.method.is_some() {
interface.method = update.method;
}
if update.method6.is_some() {
interface.method6 = update.method6;
}
if update.mtu.is_some() {
interface.mtu = update.mtu;
}
if let Some(ports) = &update.bridge_ports {
interface.set_bridge_port_list(ports)?;
}
if update.bridge_vlan_aware.is_some() {
interface.bridge_vlan_aware = update.bridge_vlan_aware;
}
if let Some(slaves) = &update.slaves {
interface.set_bond_slave_list(slaves)?;
}
if let Some(mode) = update.bond_mode {
interface.bond_mode = update.bond_mode;
if update.bond_primary.is_some() {
if mode != LinuxBondMode::ActiveBackup {
bail!("bond-primary is only valid with Active/Backup mode");
}
interface.bond_primary = update.bond_primary;
}
if update.bond_xmit_hash_policy.is_some() {
if mode != LinuxBondMode::Ieee802_3ad && mode != LinuxBondMode::BalanceXor {
bail!("bond_xmit_hash_policy is only valid with LACP(802.3ad) or balance-xor mode");
}
interface.bond_xmit_hash_policy = update.bond_xmit_hash_policy;
}
}
if let Some(cidr) = update.cidr {
let (_, _, is_v6) = network::parse_cidr(&cidr)?;
if is_v6 {
bail!("invalid address type (expected IPv4, got IPv6)");
}
interface.cidr = Some(cidr);
}
if let Some(cidr6) = update.cidr6 {
let (_, _, is_v6) = network::parse_cidr(&cidr6)?;
if !is_v6 {
bail!("invalid address type (expected IPv6, got IPv4)");
}
interface.cidr6 = Some(cidr6);
}
if let Some(gateway) = update.gateway {
let is_v6 = gateway.contains(':');
if is_v6 {
bail!("invalid address type (expected IPv4, got IPv6)");
}
interface.gateway = Some(gateway);
}
if let Some(gateway6) = update.gateway6 {
let is_v6 = gateway6.contains(':');
if !is_v6 {
bail!("invalid address type (expected IPv6, got IPv4)");
}
interface.gateway6 = Some(gateway6);
}
if update.comments.is_some() {
interface.comments = update.comments;
}
if update.comments6.is_some() {
interface.comments6 = update.comments6;
}
if interface.cidr.is_some() || interface.gateway.is_some() {
interface.method = Some(NetworkConfigMethod::Static);
} else {
interface.method = Some(NetworkConfigMethod::Manual);
}
if interface.cidr6.is_some() || interface.gateway6.is_some() {
interface.method6 = Some(NetworkConfigMethod::Static);
} else {
interface.method6 = Some(NetworkConfigMethod::Manual);
}
if update.vlan_id.is_some() {
interface.vlan_id = update.vlan_id;
}
if update.vlan_raw_device.is_some() {
interface.vlan_raw_device = update.vlan_raw_device;
}
network::save_config(&network_config)?;
Ok(())
}

View File

@ -1,532 +0,0 @@
use std::fmt;
use anyhow::{bail, Error};
use serde::{Deserialize, Serialize};
use lazy_static::lazy_static;
use regex::Regex;
use proxmox_schema::api;
use proxmox_schema::ApiStringFormat;
use proxmox_schema::ArraySchema;
use proxmox_schema::Schema;
use proxmox_schema::StringSchema;
use proxmox_schema::api_types::SAFE_ID_REGEX;
use proxmox_schema::api_types::{IP_V4_SCHEMA, IP_V6_SCHEMA};
use proxmox_schema::api_types::{CIDR_V4_SCHEMA, CIDR_V6_SCHEMA};
lazy_static! {
pub static ref PHYSICAL_NIC_REGEX: Regex = Regex::new(r"^(?:eth\d+|en[^:.]+|ib\d+)$").unwrap();
pub static ref VLAN_INTERFACE_REGEX: Regex =
Regex::new(r"^(?P<vlan_raw_device>\S+)\.(?P<vlan_id>\d+)|vlan(?P<vlan_id2>\d+)$").unwrap();
}
pub const NETWORK_INTERFACE_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&SAFE_ID_REGEX);
#[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
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,
}
}
/// Setter for bridge ports (check if interface type is a bridge)
pub fn set_bridge_ports(&mut self, ports: Vec<String>) -> Result<(), Error> {
if self.interface_type != NetworkInterfaceType::Bridge {
bail!(
"interface '{}' is no bridge (type is {:?})",
self.name,
self.interface_type
);
}
self.bridge_ports = Some(ports);
Ok(())
}
/// Setter for bridge ports (check if interface type is a bridge)
pub fn set_bridge_port_list(&mut self, ports: &str) -> Result<(), Error> {
let ports = Self::split_interface_list(ports)?;
self.set_bridge_ports(ports)
}
/// Setter for bond slaves (check if interface type is a bond)
pub fn set_bond_slaves(&mut self, slaves: Vec<String>) -> Result<(), Error> {
if self.interface_type != NetworkInterfaceType::Bond {
bail!(
"interface '{}' is no bond (type is {:?})",
self.name,
self.interface_type
);
}
self.slaves = Some(slaves);
Ok(())
}
/// Setter for bond slaves (check if interface type is a bond)
pub fn set_bond_slave_list(&mut self, slaves: &str) -> Result<(), Error> {
let slaves = Self::split_interface_list(slaves)?;
self.set_bond_slaves(slaves)
}
/// Split a network interface list into an array of interface names.
pub fn split_interface_list(list: &str) -> Result<Vec<String>, Error> {
let value = NETWORK_INTERFACE_ARRAY_SCHEMA.parse_property_string(list)?;
Ok(value
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap().to_string())
.collect())
}
}
#[api()]
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
/// Deletable property name
pub enum DeletableInterfaceProperty {
/// Delete the IPv4 address property.
Cidr,
/// Delete the IPv6 address property.
Cidr6,
/// Delete the IPv4 gateway property.
Gateway,
/// Delete the IPv6 gateway property.
Gateway6,
/// Delete the whole IPv4 configuration entry.
Method,
/// Delete the whole IPv6 configuration entry.
Method6,
/// Delete IPv4 comments
Comments,
/// Delete IPv6 comments
Comments6,
/// Delete mtu.
Mtu,
/// Delete autostart flag
Autostart,
/// Delete bridge ports (set to 'none')
#[serde(rename = "bridge_ports")]
BridgePorts,
/// Delete bridge-vlan-aware flag
#[serde(rename = "bridge_vlan_aware")]
BridgeVlanAware,
/// Delete bond-slaves (set to 'none')
Slaves,
/// Delete bond-primary
BondPrimary,
/// Delete bond transmit hash policy
#[serde(rename = "bond_xmit_hash_policy")]
BondXmitHashPolicy,
}
#[api(
properties: {
"type": {
type: NetworkInterfaceType,
optional: true,
},
autostart: {
description: "Autostart interface.",
type: bool,
optional: true,
},
method: {
type: NetworkConfigMethod,
optional: true,
},
method6: {
type: NetworkConfigMethod,
optional: true,
},
comments: {
description: "Comments (inet, may span multiple lines)",
type: String,
optional: true,
},
comments6: {
description: "Comments (inet5, may span multiple lines)",
type: String,
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,
},
mtu: {
description: "Maximum Transmission Unit.",
optional: true,
minimum: 46,
maximum: 65535,
default: 1500,
},
bridge_ports: {
schema: NETWORK_INTERFACE_LIST_SCHEMA,
optional: true,
},
bridge_vlan_aware: {
description: "Enable bridge vlan support.",
type: bool,
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,
},
slaves: {
schema: NETWORK_INTERFACE_LIST_SCHEMA,
optional: true,
},
},
)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
/// Update network interface config.
pub struct InterfaceUpdater {
#[serde(rename = "type")]
pub interface_type: Option<NetworkInterfaceType>,
pub autostart: Option<bool>,
pub method: Option<NetworkConfigMethod>,
pub method6: Option<NetworkConfigMethod>,
pub comments: Option<String>,
pub comments6: Option<String>,
pub cidr: Option<String>,
pub gateway: Option<String>,
pub cidr6: Option<String>,
pub gateway6: Option<String>,
pub mtu: Option<u64>,
#[serde(rename = "bridge_ports")]
pub bridge_ports: Option<String>,
#[serde(rename = "bridge_vlan_aware")]
pub bridge_vlan_aware: Option<bool>,
pub vlan_id: Option<u16>,
pub vlan_raw_device: Option<String>,
#[serde(rename = "bond_mode")]
pub bond_mode: Option<LinuxBondMode>,
pub bond_primary: Option<String>,
#[serde(rename = "bond_xmit_hash_policy")]
pub bond_xmit_hash_policy: Option<BondXmitHashPolicy>,
pub slaves: Option<String>,
}

View File

@ -1,232 +0,0 @@
use std::collections::HashMap;
use std::os::unix::io::{AsRawFd, FromRawFd, OwnedFd};
use std::path::Path;
use std::process::Command;
use anyhow::{bail, format_err, Error};
use const_format::concatcp;
use lazy_static::lazy_static;
use nix::ioctl_read_bad;
use nix::sys::socket::{socket, AddressFamily, SockFlag, SockType};
use regex::Regex;
use proxmox_schema::api_types::IPV4RE_STR;
use proxmox_schema::api_types::IPV6RE_STR;
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",
];
lazy_static! {
pub static ref IPV4_MASK_HASH_LOCALNET: HashMap<&'static str, u8> = {
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(crate) 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(crate) 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!
lazy_static! {
pub static ref CIDR_V4_REGEX: Regex =
Regex::new(concatcp!(r"^(", IPV4RE_STR, r")(?:/(\d{1,2}))?$")).unwrap();
pub static ref CIDR_V6_REGEX: Regex =
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(crate) 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);
lazy_static! {
static ref IFACE_LINE_REGEX: Regex = 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 = unsafe {
OwnedFd::from_raw_fd(
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(crate) 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(())
}

View File

@ -1,139 +0,0 @@
use std::collections::{HashMap, VecDeque};
use std::io::BufRead;
use std::iter::Iterator;
use lazy_static::lazy_static;
#[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,
}
lazy_static! {
static ref KEYWORDS: HashMap<&'static str, Token> = {
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,
}
}
}

View File

@ -1,723 +0,0 @@
mod helper;
mod lexer;
mod parser;
pub use helper::{assert_ifupdown2_installed, network_reload, parse_cidr};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::io::Write;
use anyhow::{bail, format_err, Error};
use lazy_static::lazy_static;
use regex::Regex;
use serde::de::{value, Deserialize, IntoDeserializer};
use super::{
BondXmitHashPolicy, Interface, LinuxBondMode, NetworkConfigMethod, NetworkInterfaceType,
};
use helper::compute_file_diff;
use helper::get_network_interfaces;
use parser::NetworkParser;
use proxmox_config_digest::ConfigDigest;
use proxmox_product_config::{open_api_lockfile, replace_system_config, ApiLockGuard};
lazy_static! {
static ref PHYSICAL_NIC_REGEX: Regex = Regex::new(r"^(?:eth\d+|en[^:.]+|ib\d+)$").unwrap();
static ref VLAN_INTERFACE_REGEX: Regex =
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)]
pub(crate) enum NetworkOrderEntry {
Iface(String),
Comment(String),
Option(String),
}
#[derive(Debug, Default)]
pub struct NetworkConfig {
pub interfaces: BTreeMap<String, Interface>,
pub(crate) 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 that there is no other gateway.
///
/// The gateway property is only allowed on passed 'iface'. This should be
/// called before setting the gateway.
pub fn check_duplicate_gateway_v4(&self, iface: &str) -> Result<(), Error> {
let current_gateway_v4 = self
.interfaces
.iter()
.find(|(_, interface)| interface.gateway.is_some())
.map(|(name, _)| name.to_string());
if let Some(current_gateway_v4) = current_gateway_v4 {
if current_gateway_v4 != iface {
bail!(
"Default IPv4 gateway already exists on interface '{}'",
current_gateway_v4
);
}
}
Ok(())
}
/// Check that there is no other v6 gateway.
///
/// The gateway6 property is only allowed on passed 'iface'. This should be
/// called before setting the gateway6.
pub fn check_duplicate_gateway_v6(&self, iface: &str) -> Result<(), Error> {
let current_gateway_v6 = self
.interfaces
.iter()
.find(|(_, interface)| interface.gateway6.is_some())
.map(|(name, _)| name.to_string());
if let Some(current_gateway_v6) = current_gateway_v6 {
if current_gateway_v6 != iface {
bail!(
"Default IPv6 gateway already exists on interface '{}'",
current_gateway_v6
);
}
}
Ok(())
}
/// 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> {
lazy_static! {
static ref VLAN_INTERFACE_REGEX: Regex = 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<ApiLockGuard, Error> {
open_api_lockfile(NETWORK_LOCKFILE, None, true)
}
pub fn config() -> Result<(NetworkConfig, ConfigDigest), 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 = ConfigDigest::from_slice(&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)?;
replace_system_config(NETWORK_INTERFACES_NEW_FILENAME, &raw)?;
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(),
format!(
r#"
iface enp3s0 inet static
address 10.0.0.100/16"#
)
.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(),
format!(
r#"
iface enp3s0 inet static
address 10.0.0.100/16
gateway 10.0.0.1"#
)
.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"));
}
}

View File

@ -1,862 +0,0 @@
use crate::network::VLAN_INTERFACE_REGEX;
use std::collections::{HashMap, HashSet};
use std::io::BufRead;
use std::iter::{Iterator, Peekable};
use anyhow::{bail, format_err, Error};
use lazy_static::lazy_static;
use regex::Regex;
use serde::de::{value, Deserialize, IntoDeserializer};
use super::helper::*;
use super::lexer::*;
use super::LinuxBondMode;
use proxmox_schema::api_types::IP_REGEX;
use super::{BondXmitHashPolicy, Interface, NetworkConfigMethod, NetworkInterfaceType};
use crate::network::config::NetworkConfig;
use crate::network::config::NetworkOrderEntry;
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))
}
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 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;
}
}
lazy_static! {
static ref INTERFACE_ALIAS_REGEX: Regex = 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.get("lo").is_none() {
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")));
}
}

View File

@ -1,13 +0,0 @@
mod api_types;
pub use api_types::*;
#[cfg(feature = "network-impl")]
mod config;
#[cfg(feature = "network-impl")]
pub use config::*;
#[cfg(feature = "network-impl")]
mod api_impl;
#[cfg(feature = "network-impl")]
pub use api_impl::{create_interface, update_interface};

View File

@ -1,58 +0,0 @@
use serde::{Deserialize, Serialize};
use proxmox_schema::api;
use proxmox_schema::api_types::SYSTEMD_DATETIME_FORMAT;
#[api(
properties: {
start: {
type: Integer,
description: "Start line number.",
minimum: 0,
optional: true,
},
limit: {
type: Integer,
description: "Max. number of lines.",
optional: true,
minimum: 0,
},
since: {
type: String,
optional: true,
description: "Display all log since this date-time string.",
format: &SYSTEMD_DATETIME_FORMAT,
},
until: {
type: String,
optional: true,
description: "Display all log until this date-time string.",
format: &SYSTEMD_DATETIME_FORMAT,
},
service: {
type: String,
optional: true,
description: "Service ID.",
max_length: 128,
},
},
)]
#[derive(Clone, PartialEq, Serialize, Deserialize)]
/// Syslog filtering options.
pub struct SyslogFilter {
pub start: Option<u64>,
pub limit: Option<u64>,
pub since: Option<String>,
pub until: Option<String>,
pub service: Option<String>,
}
#[api]
#[derive(Clone, PartialEq, Serialize, Deserialize)]
/// Syslog line with line number.
pub struct SyslogLine {
/// Line number.
pub n: u64,
/// Line text.
pub t: String,
}

View File

@ -1,73 +0,0 @@
use std::process::{Command, Stdio};
use anyhow::Error;
use super::{SyslogFilter, SyslogLine};
pub fn dump_journal(filter: SyslogFilter) -> Result<(u64, Vec<SyslogLine>), Error> {
let mut args = vec!["-o", "short", "--no-pager"];
if let Some(service) = &filter.service {
args.extend(["--unit", service]);
}
if let Some(since) = &filter.since {
args.extend(["--since", since]);
}
if let Some(until) = &filter.until {
args.extend(["--until", until]);
}
let mut lines: Vec<SyslogLine> = Vec::new();
let mut limit = filter.limit.unwrap_or(50);
let start = filter.start.unwrap_or(0);
let mut count: u64 = 0;
let mut child = Command::new("journalctl")
.args(&args)
.stdout(Stdio::piped())
.spawn()?;
use std::io::{BufRead, BufReader};
if let Some(ref mut stdout) = child.stdout {
for line in BufReader::new(stdout).lines() {
match line {
Ok(line) => {
count += 1;
if count < start {
continue;
};
if limit == 0 {
continue;
};
lines.push(SyslogLine { n: count, t: line });
limit -= 1;
}
Err(err) => {
log::error!("reading journal failed: {}", err);
let _ = child.kill();
break;
}
}
}
}
let status = child.wait().unwrap();
if !status.success() {
log::error!("journalctl failed with {}", status);
}
// HACK: ExtJS store.guaranteeRange() does not like empty array
// so we add a line
if count == 0 {
count += 1;
lines.push(SyslogLine {
n: count,
t: String::from("no content"),
});
}
Ok((count, lines))
}

View File

@ -1,8 +0,0 @@
mod api_types;
pub use api_types::*;
#[cfg(feature = "syslog-impl")]
mod journal;
#[cfg(feature = "syslog-impl")]
pub use journal::dump_journal;

View File

@ -1,29 +0,0 @@
use serde::{Deserialize, Serialize};
use proxmox_schema::api;
use proxmox_schema::api_types::TIME_ZONE_SCHEMA;
#[api(
properties: {
timezone: {
schema: TIME_ZONE_SCHEMA,
},
time: {
type: i64,
description: "Seconds since 1970-01-01 00:00:00 UTC.",
minimum: 1_297_163_644,
},
localtime: {
type: i64,
description: "Seconds since 1970-01-01 00:00:00 UTC. (local time)",
minimum: 1_297_163_644,
},
}
)]
#[derive(Serialize, Deserialize)]
/// Server time and timezone.
pub struct ServerTimeInfo {
pub timezone: String,
pub time: i64,
pub localtime: i64,
}

View File

@ -1,7 +0,0 @@
mod api_types;
pub use api_types::*;
#[cfg(feature = "time-impl")]
mod time_impl;
#[cfg(feature = "time-impl")]
pub use time_impl::*;

View File

@ -1,55 +0,0 @@
use anyhow::{bail, format_err, Error};
use proxmox_product_config::replace_system_config;
use proxmox_sys::fs::file_read_firstline;
use super::ServerTimeInfo;
pub fn read_etc_localtime() -> Result<String, Error> {
// use /etc/timezone
if let Ok(line) = file_read_firstline("/etc/timezone") {
return Ok(line.trim().to_owned());
}
// otherwise guess from the /etc/localtime symlink
let link = std::fs::read_link("/etc/localtime")
.map_err(|err| format_err!("failed to guess timezone - {}", err))?;
let link = link.to_string_lossy();
match link.rfind("/zoneinfo/") {
Some(pos) => Ok(link[(pos + 10)..].to_string()),
None => Ok(link.to_string()),
}
}
pub fn set_timezone(timezone: String) -> Result<(), Error> {
let path = std::path::PathBuf::from(format!("/usr/share/zoneinfo/{}", timezone));
if !path.exists() {
bail!("No such timezone.");
}
replace_system_config("/etc/timezone", timezone.as_bytes())?;
let _ = std::fs::remove_file("/etc/localtime");
use std::os::unix::fs::symlink;
symlink(path, "/etc/localtime")?;
Ok(())
}
/// Read server time and time zone settings.
pub fn get_server_time_info() -> Result<ServerTimeInfo, Error> {
let time = proxmox_time::epoch_i64();
let tm = proxmox_time::localtime(time)?;
let offset = tm.tm_gmtoff;
let localtime = time + offset;
Ok(ServerTimeInfo {
timezone: read_etc_localtime()?,
time: time,
localtime: localtime,
})
}