Skip to content

Commit

Permalink
Merge pull request #3220 from zmrow/add-networkd-cfg-builder
Browse files Browse the repository at this point in the history
netdog: Add networkd devices and builders for config
  • Loading branch information
zmrow authored Jul 11, 2023
2 parents 813b245 + e1b4e28 commit abd2599
Show file tree
Hide file tree
Showing 44 changed files with 1,473 additions and 2 deletions.
72 changes: 70 additions & 2 deletions sources/api/netdog/src/networkd/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,78 @@
mod netdev;
mod network;

use netdev::NetDevConfig;
use network::NetworkConfig;
use super::Result;
pub(crate) use netdev::{NetDevBuilder, NetDevConfig};
pub(crate) use network::{NetworkBuilder, NetworkConfig};

const NETWORKD_CONFIG_DIR: &str = "/etc/systemd/network";
const CONFIG_FILE_PREFIX: &str = "10-";

pub(crate) enum NetworkDConfigFile {
Network(NetworkConfig),
NetDev(NetDevConfig),
}

impl NetworkDConfigFile {
pub(crate) fn write_config_file(&self) -> Result<()> {
match self {
NetworkDConfigFile::Network(network) => network.write_config_file(NETWORKD_CONFIG_DIR),
NetworkDConfigFile::NetDev(netdev) => netdev.write_config_file(NETWORKD_CONFIG_DIR),
}
}
}

// This private module defines some empty traits meant to be used as type parameters for the
// networkd config builders. The type parameters limit the methods that can be called on the
// builders so a user of this code can't inadvertently add configuration options that aren't
// applicable to a particular device. For example, a user can't add bond monitoring options to a
// VLAN config.
//
// The following traits and enums are only meant to be used within the config module of this crate;
// putting them in a private module guarantees this behavior. See the "sealed trait" pattern here:
// https://rust-lang.github.io/api-guidelines/future-proofing.html#sealed-traits-protect-against-downstream-implementations-c-sealed
mod private {
// The following zero-variant enums represent the device types we currently support. They
// cannot be constructed and exist only as phantom types.
pub enum Bond {}
pub enum Interface {}
pub enum Vlan {}
pub enum BondWorker {} // interfaces that are bound to a bond

// The devices for which we are generating a configuration file. All device types should
// implement this trait.
pub trait Device {}
impl Device for Bond {}
impl Device for Interface {}
impl Device for Vlan {}
impl Device for BondWorker {}

// Devices not bound to a bond, i.e. everything EXCEPT BondWorker(s)
pub trait NotBonded {}
impl NotBonded for Bond {}
impl NotBonded for Interface {}
impl NotBonded for Vlan {}
}

#[cfg(test)]
mod tests {
use crate::networkd::devices::{NetworkDBond, NetworkDInterface, NetworkDVlan};
use serde::Deserialize;
use std::fs;
use std::path::{Path, PathBuf};

pub(super) const BUILDER_DATA: &str = include_str!("../../../test_data/networkd/builder.toml");

pub(super) fn test_data() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("test_data")
.join("networkd")
}

#[derive(Debug, Deserialize)]
pub(super) struct TestDevices {
pub(super) interface: Vec<NetworkDInterface>,
pub(super) bond: Vec<NetworkDBond>,
pub(super) vlan: Vec<NetworkDVlan>,
}
}
230 changes: 230 additions & 0 deletions sources/api/netdog/src/networkd/config/netdev.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
use super::private::{Bond, Device, Vlan};
use super::{CONFIG_FILE_PREFIX, NETWORKD_CONFIG_DIR};
use crate::bonding::{ArpMonitoringConfigV1, ArpValidateV1, BondModeV1, MiiMonitoringConfigV1};
use crate::interface_id::InterfaceName;
use crate::networkd::{error, Result};
use crate::vlan_id::VlanId;
use snafu::{OptionExt, ResultExt};
use std::fmt::Display;
use std::fs;
use std::marker::PhantomData;
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use systemd_derive::{SystemdUnit, SystemdUnitSection};

#[derive(Debug, Default, SystemdUnit)]
Expand Down Expand Up @@ -100,12 +108,234 @@ impl Display for ArpValidate {
#[derive(Debug)]
enum ArpAllTargets {
All,
Any,
}

impl Display for ArpAllTargets {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ArpAllTargets::All => write!(f, "all"),
ArpAllTargets::Any => write!(f, "any"),
}
}
}

impl NetDevConfig {
const FILE_EXT: &str = "netdev";

/// Write the config to the proper directory with the proper prefix and file extention
pub(crate) fn write_config_file<P: AsRef<Path>>(&self, config_dir: P) -> Result<()> {
let cfg_path = self.config_path(config_dir)?;

fs::write(&cfg_path, self.to_string()).context(error::NetworkDConfigWriteSnafu {
what: "netdev_config",
path: cfg_path,
})
}

/// Build the proper prefixed path for the config file
fn config_path<P: AsRef<Path>>(&self, config_dir: P) -> Result<PathBuf> {
let device_name = &self.netdev.as_ref().and_then(|n| n.name.clone()).context(
error::ConfigMissingNameSnafu {
what: "netdev config".to_string(),
},
)?;

let filename = format!("{}{}", CONFIG_FILE_PREFIX, device_name);
let mut path = Path::new(config_dir.as_ref()).join(filename);
path.set_extension(Self::FILE_EXT);

Ok(path)
}

// The following *mut() methods are private and primarily meant for use by the NetDevBuilder.
// They are convenience methods to access the referenced structs (which are `Option`s) since
// they may need to be accessed in multiple places during the builder's construction process.
// (And no one wants to call `get_or_insert_with()` everywhere)
fn vlan_mut(&mut self) -> &mut VlanSection {
self.vlan.get_or_insert_with(VlanSection::default)
}

fn bond_mut(&mut self) -> &mut BondSection {
self.bond.get_or_insert_with(BondSection::default)
}
}

// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=
//
/// The builder for `NetDevConfig`.
//
// Why a builder? Great question. As you can see below, some logic is involved to translate
// config struct fields to a valid NetDevConfig. Since `NetDevConfig` will be created by multiple
// devices (bonds and VLANs to start), it makes sense to centralize that logic to avoid
// duplication/mistakes. Using a builder means type parameters can be used to limit available
// methods based on the device being created. Putting the type parameter on the builder and not
// NetDevConfig avoids proliferating the type parameter everywhere NetDevConfig may be used.
#[derive(Debug)]
pub(crate) struct NetDevBuilder<T: Device> {
netdev: NetDevConfig,
spooky: PhantomData<T>,
}

impl<T: Device> NetDevBuilder<T> {
pub(crate) fn build(self) -> NetDevConfig {
self.netdev
}
}

impl NetDevBuilder<Bond> {
/// Create a new .netdev config for a bond.
pub(crate) fn new_bond(name: InterfaceName) -> Self {
let netdev = NetDevConfig {
netdev: Some(NetDevSection {
name: Some(name),
kind: Some(NetDevKind::Bond),
}),
..Default::default()
};

Self {
netdev,
spooky: PhantomData,
}
}

/// Add bond mode
pub(crate) fn with_mode(&mut self, mode: BondModeV1) {
self.netdev.bond_mut().mode = match mode {
BondModeV1::ActiveBackup => Some(BondMode::ActiveBackup),
}
}

/// Add bond minimum links
pub(crate) fn with_min_links(&mut self, min_links: usize) {
self.netdev.bond_mut().min_links = Some(min_links)
}

/// Add MIIMon configuration
pub(crate) fn with_miimon_config(&mut self, miimon: MiiMonitoringConfigV1) {
let bond = self.netdev.bond_mut();

bond.mii_mon_secs = Some(miimon.frequency);
bond.up_delay_sec = Some(miimon.updelay);
bond.down_delay_sec = Some(miimon.downdelay);
}

/// Add ARPMon configuration
pub(crate) fn with_arpmon_config(&mut self, arpmon: ArpMonitoringConfigV1) {
let bond = self.netdev.bond_mut();

// Legacy alert: wicked defaults to "any", keep that default here
// TODO: add a setting for this
bond.arp_all_targets = Some(ArpAllTargets::Any);
bond.arp_interval_secs = Some(arpmon.interval);
bond.arp_targets.extend(arpmon.targets);
bond.arp_validate = match arpmon.validate {
ArpValidateV1::Active => Some(ArpValidate::Active),
ArpValidateV1::All => Some(ArpValidate::All),
ArpValidateV1::Backup => Some(ArpValidate::Backup),
ArpValidateV1::None => Some(ArpValidate::r#None),
};
}
}

impl NetDevBuilder<Vlan> {
/// Create a new .netdev config for a VLAN
pub(crate) fn new_vlan(name: InterfaceName) -> Self {
let netdev = NetDevConfig {
netdev: Some(NetDevSection {
name: Some(name),
kind: Some(NetDevKind::Vlan),
}),
..Default::default()
};

Self {
netdev,
spooky: PhantomData,
}
}

/// Add the VLAN's ID
pub(crate) fn with_vlan_id(&mut self, id: VlanId) {
self.netdev.vlan_mut().id = Some(id);
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::bonding::BondMonitoringConfigV1;
use crate::networkd::config::tests::{test_data, TestDevices, BUILDER_DATA};
use crate::networkd::devices::{NetworkDBond, NetworkDVlan};

const FAKE_TEST_DIR: &str = "testdir";

fn netdev_path(name: String) -> PathBuf {
test_data().join("netdev").join(format!("{}.netdev", name))
}

fn netdev_from_bond(bond: NetworkDBond) -> NetDevConfig {
let mut netdev = NetDevBuilder::new_bond(bond.name.clone());
netdev.with_mode(bond.mode);
bond.min_links.map(|m| netdev.with_min_links(m));
match bond.monitoring_config {
BondMonitoringConfigV1::MiiMon(miimon) => netdev.with_miimon_config(miimon),
BondMonitoringConfigV1::ArpMon(arpmon) => netdev.with_arpmon_config(arpmon),
}
netdev.build()
}

fn netdev_from_vlan(vlan: NetworkDVlan) -> NetDevConfig {
let mut netdev = NetDevBuilder::new_vlan(vlan.name.clone());
netdev.with_vlan_id(vlan.id);
netdev.build()
}

#[test]
fn bond_netdev_builder() {
let devices = toml::from_str::<TestDevices>(BUILDER_DATA).unwrap();
for bond in devices.bond {
let expected_filename = netdev_path(bond.name.to_string());
let expected = fs::read_to_string(expected_filename).unwrap();
let got = netdev_from_bond(bond).to_string();

assert_eq!(expected, got)
}
}

#[test]
fn vlan_netdev_builder() {
let devices = toml::from_str::<TestDevices>(BUILDER_DATA).unwrap();
for vlan in devices.vlan {
let expected_filename = netdev_path(vlan.name.to_string());
let expected = fs::read_to_string(expected_filename).unwrap();
let got = netdev_from_vlan(vlan).to_string();

assert_eq!(expected, got)
}
}

#[test]
fn config_path_empty() {
let netdev = NetDevConfig::default();
assert!(netdev.config_path(FAKE_TEST_DIR).is_err())
}

#[test]
fn config_path_name() {
let filename = format!("{}foo", CONFIG_FILE_PREFIX);
let mut expected = Path::new(FAKE_TEST_DIR).join(filename);
expected.set_extension(NetDevConfig::FILE_EXT);

let netdev = NetDevConfig {
netdev: Some(NetDevSection {
name: Some(InterfaceName::try_from("foo").unwrap()),
..Default::default()
}),
..Default::default()
};

assert_eq!(expected, netdev.config_path(FAKE_TEST_DIR).unwrap())
}
}
Loading

0 comments on commit abd2599

Please sign in to comment.