From 2ecf51c3c63b385e7dc6d770ff526d1566a90fdb Mon Sep 17 00:00:00 2001 From: Zac Mrowicki Date: Tue, 12 Apr 2022 15:47:32 +0000 Subject: [PATCH 1/2] netdog: Add ability to generate network configuration This commit adds a subcommand `generate-net-config` to `netdog`. It uses a user-provided TOML file `net.toml` to generate the proper wicked configuration files for the network interfaces in the system. This file lives on the BOTTLEROCKET-PRIVATE partition. If no file is found, `netdog` looks at the kernel command line (/proc/cmdline) for the `netdog.default-interface` prefix and configures the interface listed there. A single default interface may be defined on the kernel command line with the format: `netdog.default-interface=interface name:option1,option2`. "interface name" is the name of the interface, and valid options are "dhcp4" and "dhcp6". A "?" may be added to the option to signify that the lease for the protocol is optional and the system chouldn't wait for it. If the prefix does not exist on the kernel command line, no network configuration is generated. At configuration generation time, `netdog` writes the primary interface to `/var/lib/netdog/primary_interface`. Primary interface may be set in `net.toml`; if it is absent in the file the first interface in the list is assumed to be the primary. If the interface is passed via kernel command line, it is assumed to be the default. Previously, the `netdog install` command, which is called by wicked for configured interfaces, matched on the name of the interface (eth0) before it took any action. The `install` command writes the resolv.conf, and the current_ip file. For AWS/VMware we are explicitly only configuring `eth0`, and for Metal variants we will not be able to determine the configured interface names. This commit changes the behavior. Rather than filtering on interface name in order to write `resolv.conf` and the current IP file, we only write the files if the interface currently being configured is the primary interface (read from the previously written `primary_interface` file). Note: Previously, our network configuration for `eth0` included a few options, `arp-verify` and `arp-notify`. These options were (theoretically) meant to speed up boot time in EC2 since we know our IP won't be a duplcate and therefore we don't need to ARP for it. (That's not how networking in EC2 works) In our testing, `arp-very/notify` made no significantly measureable difference in the time it took to get from "lease request" to "committed lease". From this commit on, these options aren't part of our generated config for any variant. --- sources/Cargo.lock | 5 + sources/api/netdog/Cargo.toml | 3 + sources/api/netdog/README.md | 10 + sources/api/netdog/src/interface_name.rs | 148 +++++ sources/api/netdog/src/main.rs | 124 ++++- sources/api/netdog/src/net_config.rs | 524 ++++++++++++++++++ sources/api/netdog/src/wicked.rs | 309 +++++++++++ .../test_data/cmdline/multiple_interfaces | 1 + .../netdog/test_data/cmdline/no_interfaces | 1 + sources/api/netdog/test_data/cmdline/ok | 2 + .../test_data/net_config/bad_version.toml | 26 + .../net_config/invalid_dhcp_config.toml | 3 + .../net_config/multiple_primary.toml | 28 + .../test_data/net_config/net_config.toml | 27 + .../test_data/net_config/no_interfaces.toml | 1 + .../test_data/net_config/no_primary.toml | 26 + sources/api/netdog/test_data/wicked/eno1.xml | 1 + sources/api/netdog/test_data/wicked/eno10.xml | 1 + sources/api/netdog/test_data/wicked/eno2.xml | 1 + sources/api/netdog/test_data/wicked/eno3.xml | 1 + sources/api/netdog/test_data/wicked/eno4.xml | 1 + sources/api/netdog/test_data/wicked/eno5.xml | 1 + sources/api/netdog/test_data/wicked/eno6.xml | 1 + sources/api/netdog/test_data/wicked/eno7.xml | 1 + sources/api/netdog/test_data/wicked/eno8.xml | 1 + sources/api/netdog/test_data/wicked/eno9.xml | 1 + 26 files changed, 1232 insertions(+), 16 deletions(-) create mode 100644 sources/api/netdog/src/interface_name.rs create mode 100644 sources/api/netdog/src/net_config.rs create mode 100644 sources/api/netdog/src/wicked.rs create mode 100644 sources/api/netdog/test_data/cmdline/multiple_interfaces create mode 100644 sources/api/netdog/test_data/cmdline/no_interfaces create mode 100644 sources/api/netdog/test_data/cmdline/ok create mode 100644 sources/api/netdog/test_data/net_config/bad_version.toml create mode 100644 sources/api/netdog/test_data/net_config/invalid_dhcp_config.toml create mode 100644 sources/api/netdog/test_data/net_config/multiple_primary.toml create mode 100644 sources/api/netdog/test_data/net_config/net_config.toml create mode 100644 sources/api/netdog/test_data/net_config/no_interfaces.toml create mode 100644 sources/api/netdog/test_data/net_config/no_primary.toml create mode 100644 sources/api/netdog/test_data/wicked/eno1.xml create mode 100644 sources/api/netdog/test_data/wicked/eno10.xml create mode 100644 sources/api/netdog/test_data/wicked/eno2.xml create mode 100644 sources/api/netdog/test_data/wicked/eno3.xml create mode 100644 sources/api/netdog/test_data/wicked/eno4.xml create mode 100644 sources/api/netdog/test_data/wicked/eno5.xml create mode 100644 sources/api/netdog/test_data/wicked/eno6.xml create mode 100644 sources/api/netdog/test_data/wicked/eno7.xml create mode 100644 sources/api/netdog/test_data/wicked/eno8.xml create mode 100644 sources/api/netdog/test_data/wicked/eno9.xml diff --git a/sources/Cargo.lock b/sources/Cargo.lock index ee314f7910b..52b700505d3 100644 --- a/sources/Cargo.lock +++ b/sources/Cargo.lock @@ -1714,6 +1714,7 @@ checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" dependencies = [ "autocfg", "hashbrown", + "serde", ] [[package]] @@ -2076,14 +2077,17 @@ dependencies = [ "cargo-readme", "dns-lookup", "envy", + "indexmap", "ipnet", "lazy_static", "rand", "regex", "serde", + "serde-xml-rs", "serde_json", "serde_plain", "snafu", + "toml", ] [[package]] @@ -3693,6 +3697,7 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" dependencies = [ + "indexmap", "serde", ] diff --git a/sources/api/netdog/Cargo.toml b/sources/api/netdog/Cargo.toml index 9b4f2f6c66d..9702bce5c21 100644 --- a/sources/api/netdog/Cargo.toml +++ b/sources/api/netdog/Cargo.toml @@ -12,6 +12,7 @@ exclude = ["README.md"] argh = "0.1.4" dns-lookup = "1.0" ipnet = { version = "2.0", features = ["serde"] } +indexmap = { version = "1.8", features = ["serde"]} envy = "0.4" lazy_static = "1.2" rand = { version = "0.8", default-features = false, features = ["std", "std_rng"] } @@ -19,7 +20,9 @@ regex = "1.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1" serde_plain = "1.0" +serde-xml-rs = "0.5" snafu = "0.7" +toml = { version = "0.5", features = ["preserve_order"] } [build-dependencies] cargo-readme = "3.1" diff --git a/sources/api/netdog/README.md b/sources/api/netdog/README.md index e184c09e850..43414b33251 100644 --- a/sources/api/netdog/README.md +++ b/sources/api/netdog/README.md @@ -14,6 +14,16 @@ It contains two subcommands meant for use as settings generators: The subcommand `set-hostname` sets the hostname for the system. +The subcommand `generate-net-config` generates the network interface configuration for the host. If +a `net.toml` file exists in `/var/lib/bottlerocket`, it is used to generate the configuration. If +`net.toml` doesn't exist, the kernel command line `/proc/cmdline` is checked for the prefix +`netdog.default-interface`. If an interface is defined with that prefix, it is used to generate an +interface configuration. A single default interface may be defined on the kernel command line with +the format: `netdog.default-interface=interface-name:option1,option2`. "interface-name" is the +name of the interface, and valid options are "dhcp4" and "dhcp6". A "?" may be added to the option +to signify that the lease for the protocol is optional and the system shouldn't wait for it. A +valid example: `netdog.default-interface=eno1:dhcp4,dhcp6?`. + ## Colophon This text was generated using [cargo-readme](https://crates.io/crates/cargo-readme), and includes the rustdoc from `src/main.rs`. \ No newline at end of file diff --git a/sources/api/netdog/src/interface_name.rs b/sources/api/netdog/src/interface_name.rs new file mode 100644 index 00000000000..7f7ccd61cb8 --- /dev/null +++ b/sources/api/netdog/src/interface_name.rs @@ -0,0 +1,148 @@ +//! The interface_name module contains the definition of a valid network interface name and the +//! code to support creation of the structure from string. +//! +//! A valid network interface name is defined by the criteria in the linux kernel: +//! https://elixir.bootlin.com/linux/v5.10.102/source/net/core/dev.c#L1138 +use serde::{Deserialize, Serialize, Serializer}; +use snafu::ensure; +use std::convert::TryFrom; +use std::ops::Deref; + +/// InterfaceName can only be created from a string that contains a valid network interface name. +/// Validation is handled in the `TryFrom` implementation below. +#[derive(Debug, Eq, PartialEq, Hash, Deserialize)] +#[serde(try_from = "&str")] +pub(crate) struct InterfaceName { + inner: String, +} + +impl TryFrom<&str> for InterfaceName { + type Error = error::Error; + + fn try_from(input: &str) -> Result { + // Rust does not treat all Unicode line terminators as starting a new line, so we check for + // specific characters here, rather than just counting from lines(). + // https://en.wikipedia.org/wiki/Newline#Unicode + let line_terminators = [ + '\n', // newline (0A) + '\r', // carriage return (0D) + '\u{000B}', // vertical tab + '\u{000C}', // form feed + '\u{0085}', // next line + '\u{2028}', // line separator + '\u{2029}', // paragraph separator + ]; + + ensure!( + !input.contains(&line_terminators[..]), + error::InvalidNetworkDeviceNameSnafu { + input, + msg: "contains line terminators" + } + ); + + // The length for an interface name is defined here: + // https://elixir.bootlin.com/linux/v5.10.102/source/include/uapi/linux/if.h#L33 + // The constant definition (16) is a little misleading as the check for it ensures that the + // name is NOT equal to 16. A name must be 1-15 characters. + ensure!( + !input.is_empty() && input.len() <= 15, + error::InvalidNetworkDeviceNameSnafu { + input, + msg: "invalid length, must be 1 to 15 characters long" + } + ); + + ensure!( + !input.contains('.') && !input.contains('/') && !input.contains(char::is_whitespace), + error::InvalidNetworkDeviceNameSnafu { + input, + msg: "contains invalid characters" + } + ); + + Ok(Self { + inner: input.to_string(), + }) + } +} + +impl TryFrom for InterfaceName { + type Error = error::Error; + + fn try_from(input: String) -> Result { + Self::try_from(input.as_ref()) + } +} + +impl Deref for InterfaceName { + type Target = str; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl Serialize for InterfaceName { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(&self.inner) + } +} + +mod error { + use snafu::Snafu; + + #[derive(Debug, Snafu)] + #[snafu(visibility(pub(crate)))] + pub(crate) enum Error { + #[snafu(display("Invalid network device name '{}': {}", input, msg))] + InvalidNetworkDeviceName { input: String, msg: String }, + } +} + +pub(crate) use error::Error; +type Result = std::result::Result; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn invalid_interface_name() { + let bad_str = [ + &std::iter::repeat("a").take(16).collect::(), + "", + ".", + "..", + "f/eno1", + "eno 1", + "eno\n1", + "\n", + "\r", + "\u{000B}", + "\u{000C}", + "\u{0085}", + "\u{2028}", + "\u{2029}", + ]; + for bad in bad_str { + assert!(InterfaceName::try_from(bad).is_err()) + } + } + + #[test] + fn valid_interface_name() { + let ok_str = [ + &std::iter::repeat("a").take(15).collect::(), + "eno1", + "eth0", + "enp5s0", + "enx0eb36944b633", + ]; + for ok in ok_str { + assert!(InterfaceName::try_from(ok).is_ok()) + } + } +} diff --git a/sources/api/netdog/src/main.rs b/sources/api/netdog/src/main.rs index 92b0eaa4fef..bcb4392c599 100644 --- a/sources/api/netdog/src/main.rs +++ b/sources/api/netdog/src/main.rs @@ -10,6 +10,16 @@ It contains two subcommands meant for use as settings generators: * `generate-hostname`: returns the node's hostname in JSON format. If the lookup is unsuccessful, the IP of the node is used. The subcommand `set-hostname` sets the hostname for the system. + +The subcommand `generate-net-config` generates the network interface configuration for the host. If +a `net.toml` file exists in `/var/lib/bottlerocket`, it is used to generate the configuration. If +`net.toml` doesn't exist, the kernel command line `/proc/cmdline` is checked for the prefix +`netdog.default-interface`. If an interface is defined with that prefix, it is used to generate an +interface configuration. A single default interface may be defined on the kernel command line with +the format: `netdog.default-interface=interface-name:option1,option2`. "interface-name" is the +name of the interface, and valid options are "dhcp4" and "dhcp6". A "?" may be added to the option +to signify that the lease for the protocol is optional and the system shouldn't wait for it. A +valid example: `netdog.default-interface=eno1:dhcp4,dhcp6?`. */ #![deny(rust_2018_idioms)] @@ -17,16 +27,21 @@ The subcommand `set-hostname` sets the hostname for the system. #[macro_use] extern crate serde_plain; +mod interface_name; +mod net_config; +mod wicked; + use argh::FromArgs; use dns_lookup::lookup_addr; use envy; use ipnet::IpNet; use lazy_static::lazy_static; +use net_config::NetConfig; use rand::seq::SliceRandom; use rand::thread_rng; use regex::Regex; use serde::{Deserialize, Serialize}; -use snafu::ResultExt; +use snafu::{OptionExt, ResultExt}; use std::collections::BTreeSet; use std::fmt::Write; use std::fs::{self, File}; @@ -35,10 +50,14 @@ use std::net::IpAddr; use std::path::{Path, PathBuf}; use std::process; use std::str::FromStr; +use wicked::WickedInterface; static RESOLV_CONF: &str = "/etc/resolv.conf"; static KERNEL_HOSTNAME: &str = "/proc/sys/kernel/hostname"; static CURRENT_IP: &str = "/var/lib/netdog/current_ip"; +static KERNEL_CMDLINE: &str = "/proc/cmdline"; +static PRIMARY_INTERFACE: &str = "/var/lib/netdog/primary_interface"; +static DEFAULT_NET_CONFIG_FILE: &str = "/var/lib/bottlerocket/net.toml"; // Matches wicked's shell-like syntax for DHCP lease variables: // FOO='BAR' -> key=FOO, val=BAR @@ -60,12 +79,6 @@ struct LeaseInfo { dns_search: Option>, } -#[derive(Debug, PartialEq, Deserialize)] -#[serde(rename_all = "kebab-case")] -enum InterfaceName { - Eth0, -} - #[derive(Debug, PartialEq, Deserialize)] #[serde(rename_all = "kebab-case")] enum InterfaceType { @@ -80,7 +93,6 @@ enum InterfaceFamily { } // Implement `from_str()` so argh can attempt to deserialize args into their proper types -derive_fromstr_from_deserialize!(InterfaceName); derive_fromstr_from_deserialize!(InterfaceType); derive_fromstr_from_deserialize!(InterfaceFamily); @@ -98,6 +110,7 @@ enum SubCommand { Remove(RemoveArgs), NodeIp(NodeIpArgs), GenerateHostname(GenerateHostnameArgs), + GenerateNetConfig(GenerateNetConfigArgs), SetHostname(SetHostnameArgs), } @@ -107,7 +120,7 @@ enum SubCommand { struct InstallArgs { #[argh(option, short = 'i')] /// name of the network interface - interface_name: InterfaceName, + interface_name: String, #[argh(option, short = 't')] /// network interface type @@ -136,7 +149,7 @@ struct InstallArgs { struct RemoveArgs { #[argh(option, short = 'i')] /// name of the network interface - interface_name: InterfaceName, + interface_name: String, #[argh(option, short = 't')] /// network interface type @@ -157,6 +170,11 @@ struct NodeIpArgs {} /// Generate hostname from DNS reverse lookup or use current IP struct GenerateHostnameArgs {} +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand, name = "generate-net-config")] +/// Generate wicked network configuration +struct GenerateNetConfigArgs {} + #[derive(FromArgs, PartialEq, Debug)] #[argh(subcommand, name = "set-hostname")] /// Sets the hostname @@ -220,12 +238,21 @@ fn write_current_ip(ip: &IpAddr) -> Result<()> { } fn install(args: InstallArgs) -> Result<()> { - match ( - &args.interface_name, - &args.interface_type, - &args.interface_family, - ) { - (InterfaceName::Eth0, InterfaceType::Dhcp, InterfaceFamily::Ipv4) => { + // Wicked doesn't mangle interface names, but let's be defensive. + let install_interface = args.interface_name.trim().to_lowercase(); + let primary_interface = fs::read_to_string(PRIMARY_INTERFACE) + .context(error::PrimaryInterfaceReadSnafu { + path: PRIMARY_INTERFACE, + })? + .trim() + .to_lowercase(); + + if install_interface != primary_interface { + return Ok(()); + } + + match (&args.interface_type, &args.interface_family) { + (InterfaceType::Dhcp, InterfaceFamily::Ipv4) => { let info = parse_lease_info(&args.data_file)?; // Randomize name server order, for libc implementations like musl that send // queries to the first N servers. @@ -280,6 +307,51 @@ fn generate_hostname() -> Result<()> { Ok(print_json(hostname)?) } +/// Generate configuration for network interfaces. +fn generate_net_config() -> Result<()> { + let maybe_net_config = if Path::exists(Path::new(DEFAULT_NET_CONFIG_FILE)) { + NetConfig::from_path(DEFAULT_NET_CONFIG_FILE).context(error::NetConfigParseSnafu { + path: DEFAULT_NET_CONFIG_FILE, + })? + } else { + NetConfig::from_command_line(KERNEL_CMDLINE).context(error::NetConfigParseSnafu { + path: KERNEL_CMDLINE, + })? + }; + + // `maybe_net_config` could be `None` if no interfaces were defined + let net_config = match maybe_net_config { + Some(net_config) => net_config, + None => { + eprintln!("No network interfaces were configured"); + return Ok(()); + } + }; + let primary_interface = net_config + .primary_interface() + .context(error::GetPrimaryInterfaceSnafu)?; + write_primary_interface(primary_interface)?; + + for (name, config) in net_config.interfaces { + let wicked_interface = WickedInterface::from_config(name, config); + wicked_interface + .write_config_file() + .context(error::InterfaceConfigWriteSnafu)?; + } + Ok(()) +} + +/// Persist the primary interface name to file +fn write_primary_interface(interface: S) -> Result<()> +where + S: AsRef, +{ + let interface = interface.as_ref(); + fs::write(PRIMARY_INTERFACE, interface).context(error::PrimaryInterfaceWriteSnafu { + path: PRIMARY_INTERFACE, + }) +} + /// Helper function that serializes the input to JSON and prints it fn print_json(val: S) -> Result<()> where @@ -306,6 +378,7 @@ fn run() -> Result<()> { SubCommand::Remove(args) => remove(args)?, SubCommand::NodeIp(_) => node_ip()?, SubCommand::GenerateHostname(_) => generate_hostname()?, + SubCommand::GenerateNetConfig(_) => generate_net_config()?, SubCommand::SetHostname(args) => set_hostname(args)?, } Ok(()) @@ -323,6 +396,7 @@ fn main() { /// Potential errors during netdog execution mod error { + use crate::{net_config, wicked}; use envy; use snafu::Snafu; use std::io; @@ -364,6 +438,24 @@ mod error { output: String, source: serde_json::error::Error, }, + + #[snafu(display("Unable to read/parse network config from '{}': {}", path.display(), source))] + NetConfigParse { + path: PathBuf, + source: net_config::Error, + }, + + #[snafu(display("Failed to write network interface configuration: {}", source))] + InterfaceConfigWrite { source: wicked::Error }, + + #[snafu(display("Failed to write primary interface to '{}': {}", path.display(), source))] + PrimaryInterfaceWrite { path: PathBuf, source: io::Error }, + + #[snafu(display("Failed to read primary interface to '{}': {}", path.display(), source))] + PrimaryInterfaceRead { path: PathBuf, source: io::Error }, + + #[snafu(display("Failed to discern primary interface"))] + GetPrimaryInterface, } } diff --git a/sources/api/netdog/src/net_config.rs b/sources/api/netdog/src/net_config.rs new file mode 100644 index 00000000000..057d399e2b1 --- /dev/null +++ b/sources/api/netdog/src/net_config.rs @@ -0,0 +1,524 @@ +//! The net_config module contains the strucures needed to deserialize a `net.toml` file. It also +//! includes contains the `FromStr` implementations to create a `NetConfig` from string, like from +//! the kernel command line. +//! +//! These structures are the user-facing options for configuring one or more network interfaces. +use crate::interface_name::InterfaceName; +use indexmap::{indexmap, IndexMap}; +use serde::Deserialize; +use snafu::{ensure, OptionExt, ResultExt}; +use std::collections::HashSet; +use std::convert::TryInto; +use std::fs; +use std::ops::Deref; +use std::path::Path; +use std::str::FromStr; + +static DEFAULT_INTERFACE_PREFIX: &str = "netdog.default-interface="; + +// TODO: support deserializing different versions of this configuration. +// Idea: write a deserializer that uses the `version` field and deserializes the rest of the config +// into an enum with variants for each version, i.e. +// enum NetConfig { +// V1(NetInterfaceV1) +// V2(NetInterfaceV2) +// } +#[derive(Debug, Deserialize)] +pub(crate) struct NetConfig { + pub(crate) version: u8, + // Use an IndexMap to preserve the order of the devices defined in the net.toml. The TOML + // library supports this through a feature making use of IndexMap. Order is important because + // we use the first device in the list as the primary device if the `primary` key isn't set for + // any of the devices. + // + // A custom type is used here that will ensure the validity of the interface name as according + // to the criteria in the linux kernel. See the `interface_name` module for additional details + // on the validation performed. + #[serde(flatten)] + pub(crate) interfaces: IndexMap, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct NetInterface { + // Use this interface as the primary interface for the system + pub(crate) primary: Option, + pub(crate) dhcp4: Option, + pub(crate) dhcp6: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub(crate) enum Dhcp4Config { + DhcpEnabled(bool), + WithOptions(Dhcp4Options), +} + +#[derive(Debug, Deserialize)] +pub(crate) struct Dhcp4Options { + pub(crate) enabled: Option, + pub(crate) optional: Option, + #[serde(rename = "route-metric")] + pub(crate) route_metric: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub(crate) enum Dhcp6Config { + DhcpEnabled(bool), + WithOptions(Dhcp6Options), +} + +#[derive(Debug, Deserialize)] +pub(crate) struct Dhcp6Options { + pub(crate) enabled: Option, + pub(crate) optional: Option, +} + +impl NetConfig { + /// Create a `NetConfig` from file + pub(crate) fn from_path

(path: P) -> Result> + where + P: AsRef, + { + let path = path.as_ref(); + let net_config_str = + fs::read_to_string(path).context(error::NetConfigReadFailedSnafu { path })?; + let net_config: NetConfig = + toml::from_str(&net_config_str).context(error::NetConfigParseSnafu { path })?; + + ensure!( + net_config.version == 1, + error::InvalidNetConfigSnafu { + reason: "invalid version" + } + ); + + let dhcp_misconfigured = net_config + .interfaces + .values() + .any(|cfg| cfg.dhcp4.is_none() && cfg.dhcp6.is_none()); + ensure!( + !dhcp_misconfigured, + error::InvalidNetConfigSnafu { + reason: "each interface must configure dhcp4 or dhcp6, or both", + } + ); + + let primary_count = net_config + .interfaces + .values() + .filter(|v| v.primary == Some(true)) + .count(); + ensure!( + primary_count <= 1, + error::InvalidNetConfigSnafu { + reason: "multiple primary interfaces defined, expected 1" + } + ); + + if net_config.interfaces.is_empty() { + return Ok(None); + } + + Ok(Some(net_config)) + } + + /// Create a `NetConfig` from string from the kernel command line + pub(crate) fn from_command_line

(path: P) -> Result> + where + P: AsRef, + { + let p = path.as_ref(); + let kernel_cmdline = + fs::read_to_string(p).context(error::KernelCmdlineReadFailedSnafu { path: p })?; + + let mut maybe_interfaces = kernel_cmdline + .split_whitespace() + .filter(|s| s.starts_with(DEFAULT_INTERFACE_PREFIX)); + + let default_interface = match maybe_interfaces.next() { + Some(interface_str) => interface_str + .trim_start_matches(DEFAULT_INTERFACE_PREFIX) + .to_string(), + None => return Ok(None), + }; + + ensure!( + maybe_interfaces.next().is_none(), + error::MultipleDefaultInterfacesSnafu + ); + + let net_config = NetConfig::from_str(&default_interface)?; + Ok(Some(net_config)) + } + + /// Return the primary interface for the system. If none of the interfaces are defined as + /// `primary = true`, we use the first interface in the configuration file. Returns `None` in + /// the case no interfaces are defined. + pub(crate) fn primary_interface(&self) -> Option { + self.interfaces + .iter() + .find(|(_, v)| v.primary == Some(true)) + .or_else(|| self.interfaces.first()) + .map(|(n, _)| n.to_string()) + } +} + +// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= + +/// Allow a simple network configuration definition to be parsed from a string. The expected input +/// string looks like: `interface-name:option1,option2`. The colon is required. Acceptable +/// options are "dhcp4", and "dhcp6". For both options an additional sigil, "?", may be provided +/// to signify that the protocol is optional. "Optional" in this context means that we will not +/// wait for a lease in order to consider the interface operational. +/// +/// An full and sensible example could look like: `eno1:dhcp4,dhcp6?`. This would create an +/// interface configuration for the interface named `eno1`, enable both dhcp4 and dhcp6, and +/// consider a dhcp6 lease optional. +impl FromStr for NetConfig { + type Err = Error; + + fn from_str(s: &str) -> std::result::Result { + let (name, options) = s + .split_once(":") + .context(error::InvalidInterfaceDefSnafu { definition: s })?; + + if options.is_empty() || name.is_empty() { + return error::InvalidInterfaceDefSnafu { definition: s }.fail(); + } + + let name = name.try_into().context(error::InvalidInterfaceNameSnafu)?; + let mut interface_config = NetInterface { + primary: None, + dhcp4: None, + dhcp6: None, + }; + + // Keep track of the options we've parsed, and fail if an option is passed more than once, + // for example "dhcp4,dhcp4?" + let mut provided_options = HashSet::new(); + for option in options.split(',').collect::>() { + if provided_options.contains(option) { + return error::InvalidInterfaceDefSnafu { definition: s }.fail(); + } + + if option.starts_with("dhcp4") { + provided_options.insert("dhcp4"); + interface_config.dhcp4 = Some(Dhcp4Config::from_str(option)?) + } else if option.starts_with("dhcp6") { + provided_options.insert("dhcp6"); + interface_config.dhcp6 = Some(Dhcp6Config::from_str(option)?) + } else { + return error::InvalidInterfaceOptionSnafu { given: option }.fail(); + } + } + + let interfaces = indexmap! {name => interface_config}; + let net_config = NetConfig { + version: 1, + interfaces, + }; + Ok(net_config) + } +} + +/// Parse Dhcp4 configuration from a string. See the `FromStr` impl for `NetConfig` for +/// additional details. +/// +/// The expected input here is a string beginning with `dhcp4`. +impl FromStr for Dhcp4Config { + type Err = Error; + + fn from_str(s: &str) -> std::result::Result { + ensure!( + s.starts_with("dhcp4"), + error::CreateFromStrSnafu { + what: "Dhcp4 options", + given: s + } + ); + + let mut optional = None; + let maybe_sigils = s.trim_start_matches("dhcp4"); + if !maybe_sigils.is_empty() { + let sigils = Sigils::from_str(maybe_sigils)?; + for sigil in &*sigils { + match sigil { + Sigil::Optional => { + optional = Some(true); + } + } + } + } + + let dhcp4_options = Dhcp4Options { + enabled: Some(true), + optional, + route_metric: None, + }; + Ok(Dhcp4Config::WithOptions(dhcp4_options)) + } +} + +/// Parse Dhcp6 configuration from a string. See the `FromStr` impl for `NetConfig` for +/// additional details. +/// +/// The expected input here is a string beginning with `dhcp6`. +impl FromStr for Dhcp6Config { + type Err = Error; + + fn from_str(s: &str) -> std::result::Result { + ensure!( + s.starts_with("dhcp6"), + error::CreateFromStrSnafu { + what: "Dhcp6 options", + given: s + } + ); + + let mut optional = None; + let maybe_sigils = s.trim_start_matches("dhcp6"); + if !maybe_sigils.is_empty() { + let sigils = Sigils::from_str(maybe_sigils)?; + for sigil in &*sigils { + match sigil { + Sigil::Optional => { + optional = Some(true); + } + } + } + } + + let dhcp6_options = Dhcp6Options { + enabled: Some(true), + optional, + }; + Ok(Dhcp6Config::WithOptions(dhcp6_options)) + } +} + +/// A wrapper around the possible sigils meant to configure dhcp4 and dhcp6 for an interface. These +/// sigils will be parsed as part of an interface directive string, e.g. "dhcp4?". Currently only +/// "Optional" is supported ("?"). +#[derive(Debug)] +enum Sigil { + Optional, +} + +#[derive(Debug)] +struct Sigils(Vec); + +// This is mostly for convenience to allow iterating over the contained Vec +impl Deref for Sigils { + type Target = Vec; + + fn deref(&self) -> &Vec { + &self.0 + } +} + +impl FromStr for Sigils { + type Err = Error; + + fn from_str(s: &str) -> std::result::Result { + let mut sigils = Sigils(Vec::new()); + + // `chars()` won't give us grapheme clusters, but we don't support any exotic sigils so + // chars should be fine here + let sigil_chars = s.chars(); + for sigil in sigil_chars { + match sigil { + '?' => sigils.0.push(Sigil::Optional), + _ => { + return error::CreateFromStrSnafu { + what: "sigils", + given: sigil, + } + .fail() + } + } + } + + Ok(sigils) + } +} + +// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= + +mod error { + use crate::interface_name; + use snafu::Snafu; + use std::io; + use std::path::PathBuf; + + #[derive(Debug, Snafu)] + #[snafu(visibility(pub(crate)))] + pub(crate) enum Error { + #[snafu(display("Unable to create '{}', from '{}'", what, given))] + CreateFromStr { what: String, given: String }, + + #[snafu(display( + "Invalid interface definition, expected 'name:option1,option2', got {}", + definition + ))] + InvalidInterfaceDef { definition: String }, + + #[snafu(display("Invalid interface name: {}", source))] + InvalidInterfaceName { source: interface_name::Error }, + + #[snafu(display( + "Invalid interface option, expected 'dhcp4' or 'dhcp6', got '{}'", + given + ))] + InvalidInterfaceOption { given: String }, + + #[snafu(display("Invalid network configuration: {}", reason))] + InvalidNetConfig { reason: String }, + + #[snafu(display("Failed to read kernel command line from '{}': {}", path.display(), source))] + KernelCmdlineReadFailed { path: PathBuf, source: io::Error }, + + #[snafu(display( + "Multiple default interfaces defined on kernel command line, expected 1", + ))] + MultipleDefaultInterfaces, + + #[snafu(display("Failed to read network config from '{}': {}", path.display(), source))] + NetConfigReadFailed { path: PathBuf, source: io::Error }, + + #[snafu(display("Failed to parse network config from '{}': {}", path.display(), source))] + NetConfigParse { + path: PathBuf, + source: toml::de::Error, + }, + } +} + +pub(crate) use error::Error; +type Result = std::result::Result; + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::*; + + fn test_data() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_data") + } + + fn cmdline() -> PathBuf { + test_data().join("cmdline") + } + + fn net_config() -> PathBuf { + test_data().join("net_config") + } + + #[test] + fn ok_cmdline() { + let cmdline = cmdline().join("ok"); + assert!(NetConfig::from_command_line(cmdline).unwrap().is_some()); + } + + #[test] + fn multiple_interface_from_cmdline() { + let cmdline = cmdline().join("multiple_interface"); + assert!(NetConfig::from_command_line(cmdline).is_err()) + } + + #[test] + fn no_interfaces_cmdline() { + let cmdline = cmdline().join("no_interfaces"); + assert!(NetConfig::from_command_line(cmdline).unwrap().is_none()) + } + + #[test] + fn invalid_version() { + let bad = net_config().join("bad_version.toml"); + assert!(NetConfig::from_path(bad).is_err()) + } + + #[test] + fn ok_config() { + let ok = net_config().join("net_config.toml"); + assert!(NetConfig::from_path(ok).is_ok()) + } + + #[test] + fn invalid_dhcp_config() { + let ok = net_config().join("invalid_dhcp_config.toml"); + assert!(NetConfig::from_path(ok).is_err()) + } + + #[test] + fn no_interfaces() { + let bad = net_config().join("no_interfaces.toml"); + assert!(NetConfig::from_path(bad).unwrap().is_none()) + } + + #[test] + fn defined_primary_interface() { + let ok_path = net_config().join("net_config.toml"); + let cfg = NetConfig::from_path(ok_path).unwrap().unwrap(); + + let expected = "eno2"; + let actual = cfg.primary_interface().unwrap(); + assert_eq!(expected, actual) + } + + #[test] + fn undefined_primary_interface() { + let ok_path = net_config().join("no_primary.toml"); + let cfg = NetConfig::from_path(ok_path).unwrap().unwrap(); + + let expected = "eno3"; + let actual = cfg.primary_interface().unwrap(); + println!("{}", &actual); + assert_eq!(expected, actual) + } + + #[test] + fn multiple_primary_interfaces() { + let multiple = net_config().join("multiple_primary.toml"); + assert!(NetConfig::from_path(multiple).is_err()) + } + + #[test] + fn ok_interface_from_str() { + let ok = &[ + "eno1:dhcp4,dhcp6", + "eno1:dhcp4,dhcp6?", + "eno1:dhcp4?,dhcp6", + "eno1:dhcp4?,dhcp6?", + "eno1:dhcp6?,dhcp4?", + "eno1:dhcp4", + "eno1:dhcp4?", + "eno1:dhcp6", + "eno1:dhcp6?", + ]; + for ok_str in ok { + assert!(NetConfig::from_str(ok_str).is_ok()) + } + } + + #[test] + fn invalid_interface_from_str() { + let bad = &[ + "", + ":", + "eno1:", + ":dhcp4,dhcp6", + "dhcp4", + "eno1:dhc4", + "eno1:dhcp", + "eno1:dhcp4+", + "eno1:dhcp?", + "eno1:dhcp4?,dhcp4", + "ENO1:DHCP4?,DhCp6", + ]; + for bad_str in bad { + assert!(NetConfig::from_str(bad_str).is_err()) + } + } +} diff --git a/sources/api/netdog/src/wicked.rs b/sources/api/netdog/src/wicked.rs new file mode 100644 index 00000000000..2915e9503fd --- /dev/null +++ b/sources/api/netdog/src/wicked.rs @@ -0,0 +1,309 @@ +//! The wicked module contains the data structures and functions needed to create network interface +//! configuration files for wicked. +//! +//! The structures in this module are meant to be created from the user-facing structures in the +//! `net_config` module. `Default` implementations for WickedInterface exist here as well. +use crate::interface_name::InterfaceName; +use crate::net_config::{Dhcp4Config, Dhcp4Options, Dhcp6Config, Dhcp6Options, NetInterface}; +use serde::Serialize; +use snafu::ResultExt; +use std::fs; +use std::path::Path; + +const WICKED_CONFIG_DIR: &str = "/etc/wicked/ifconfig"; +const WICKED_FILE_EXT: &str = "xml"; + +#[derive(Debug, Serialize, PartialEq)] +#[serde(rename = "interface")] +pub(crate) struct WickedInterface { + name: InterfaceName, + control: WickedControl, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "ipv4:dhcp")] + ipv4_dhcp: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "ipv6:dhcp")] + ipv6_dhcp: Option, +} + +#[derive(Debug, Serialize, PartialEq)] +#[serde(rename_all = "kebab-case")] +struct WickedControl { + #[serde(skip_serializing_if = "Option::is_none")] + mode: Option, + #[serde(skip_serializing_if = "Option::is_none")] + link_detection: Option, + // TODO: `serde_xml_rs` has a known issue with serializing nested structures, where it will + // insert additional tag with the structure name. It has since been fixed but not released + // officially yet. This struct member works around that issue. + // https://github.com/RReverser/serde-xml-rs/issues/126 + // The workaround: + // https://stackoverflow.com/questions/70124048/how-to-create-xml-from-struct-in-rust + #[serde(flatten, skip)] + _f: (), +} + +// We assume that all configured interfaces are wanted at boot and will require a link to +// be considered configured +impl Default for WickedControl { + fn default() -> Self { + WickedControl { + mode: Some("boot".to_string()), + link_detection: Some(LinkDetection::default()), + _f: (), + } + } +} + +#[derive(Default, Debug, Serialize, PartialEq)] +#[serde(rename_all = "kebab-case")] +struct LinkDetection { + // This will serialize to an empty tag + require_link: (), + #[serde(flatten, skip)] + _f: (), +} + +#[derive(Debug, Clone, Serialize, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub(crate) struct WickedDhcp4 { + #[serde(skip_serializing_if = "Option::is_none")] + enabled: Option, + #[serde(skip_serializing_if = "Option::is_none")] + route_priority: Option, + #[serde(skip_serializing_if = "Option::is_none")] + defer_timeout: Option, + #[serde(skip_serializing_if = "Option::is_none")] + flags: Option, + #[serde(flatten, skip)] + _f: (), +} + +impl Default for WickedDhcp4 { + fn default() -> Self { + WickedDhcp4 { + enabled: Some(true), + route_priority: None, + defer_timeout: None, + flags: None, + _f: (), + } + } +} + +#[derive(Debug, Clone, Serialize, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub(crate) struct WickedDhcp6 { + #[serde(skip_serializing_if = "Option::is_none")] + enabled: Option, + #[serde(skip_serializing_if = "Option::is_none")] + defer_timeout: Option, + #[serde(skip_serializing_if = "Option::is_none")] + flags: Option, + #[serde(flatten, skip)] + _f: (), +} + +impl Default for WickedDhcp6 { + fn default() -> Self { + WickedDhcp6 { + enabled: Some(true), + defer_timeout: None, + flags: None, + _f: (), + } + } +} + +// This is technically an enum, but considering we don't expose anything other than "optional" to +// the user, a struct makes handling tags much simpler. +#[derive(Default, Clone, Debug, Serialize, PartialEq)] +struct AddrConfFlags { + optional: (), + #[serde(flatten, skip)] + _f: (), +} + +impl From for WickedDhcp4 { + fn from(dhcp4: Dhcp4Config) -> Self { + match dhcp4 { + Dhcp4Config::DhcpEnabled(b) => WickedDhcp4 { + enabled: Some(b), + _f: (), + ..Default::default() + }, + Dhcp4Config::WithOptions(o) => WickedDhcp4::from(o), + } + } +} + +impl From for WickedDhcp4 { + fn from(options: Dhcp4Options) -> Self { + let mut defer_timeout = None; + let mut flags = None; + + if options.optional == Some(true) { + defer_timeout = Some(1); + flags = Some(AddrConfFlags::default()); + } + + WickedDhcp4 { + enabled: options.enabled, + route_priority: options.route_metric, + defer_timeout, + flags, + _f: (), + } + } +} + +impl From for WickedDhcp6 { + fn from(dhcp6: Dhcp6Config) -> Self { + match dhcp6 { + Dhcp6Config::DhcpEnabled(b) => WickedDhcp6 { + enabled: Some(b), + _f: (), + ..Default::default() + }, + Dhcp6Config::WithOptions(o) => WickedDhcp6::from(o), + } + } +} + +impl From for WickedDhcp6 { + fn from(options: Dhcp6Options) -> Self { + let mut defer_timeout = None; + let mut flags = None; + + if options.optional == Some(true) { + defer_timeout = Some(1); + flags = Some(AddrConfFlags::default()); + } + + WickedDhcp6 { + enabled: options.enabled, + defer_timeout, + flags, + _f: (), + } + } +} + +impl WickedInterface { + /// Create a WickedInterface given a name and configuration + pub(crate) fn from_config(name: InterfaceName, config: NetInterface) -> Self { + let wicked_dhcp4 = config.dhcp4.map(WickedDhcp4::from); + // As additional options are added for IPV6, implement `From` similar to WickedDhcp4 + let wicked_dhcp6 = config.dhcp6.map(WickedDhcp6::from); + WickedInterface { + name, + control: WickedControl::default(), + ipv4_dhcp: wicked_dhcp4, + ipv6_dhcp: wicked_dhcp6, + } + } + + /// Serialize the interface's configuration file + // Consume `self` to enforce that changes aren't made to the interface type after it has been + // written to file + pub(crate) fn write_config_file(&self) -> Result<()> { + let mut cfg_path = Path::new(WICKED_CONFIG_DIR).join(self.name.to_string()); + cfg_path.set_extension(WICKED_FILE_EXT); + + // TODO: pretty print these files. `serde_xml_rs` doesn't support pretty printing; + // `quick_xml` does, however we require a few features that haven't been released yet to + // properly serialize the above data structures: + // https://github.com/tafia/quick-xml/issues/340#issuecomment-981093602 + let xml = serde_xml_rs::to_string(&self).context(error::XmlSerializeSnafu { + interface: self.name.to_string(), + })?; + fs::write(&cfg_path, xml).context(error::WickedConfigWriteSnafu { path: cfg_path }) + } +} + +mod error { + use snafu::Snafu; + use std::io; + use std::path::PathBuf; + + #[derive(Debug, Snafu)] + #[snafu(visibility(pub(crate)))] + pub(crate) enum Error { + #[snafu(display("Failed to write network configuration to '{}': {}", path.display(), source))] + WickedConfigWrite { path: PathBuf, source: io::Error }, + + #[snafu(display("Error serializing config for '{}' to XML: {}", interface, source))] + XmlSerialize { + interface: String, + source: serde_xml_rs::Error, + }, + } +} + +pub(crate) use error::Error; +type Result = std::result::Result; + +#[cfg(test)] +mod tests { + use super::*; + use crate::net_config::NetConfig; + use std::path::PathBuf; + use std::str::FromStr; + + fn test_data() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("test_data") + .join("wicked") + } + + // Test the end-to-end trip: "net config from cmdline -> wicked -> serialized XML" + #[test] + fn interface_config_from_str() { + // Interface names here coincide with config files, some of which are shared with the + // `net_config` test below + let ok = &[ + "eno1:dhcp4", + "eno2:dhcp6", + "eno9:dhcp4?", + "eno10:dhcp6?", + "eno5:dhcp4,dhcp6", + "eno5:dhcp6,dhcp4", + "eno7:dhcp4,dhcp6?", + "eno7:dhcp6?,dhcp4", + "eno8:dhcp6?,dhcp4?", + "eno8:dhcp4?,dhcp6?", + ]; + for ok_str in ok { + let net_config = NetConfig::from_str(&ok_str).unwrap(); + + for (name, config) in net_config.interfaces { + let interface = WickedInterface::from_config(name, config); + let generated = serde_xml_rs::to_string(&interface).unwrap(); + + let mut path = test_data().join(interface.name.to_string()); + path.set_extension("xml"); + let expected = fs::read_to_string(path).unwrap(); + + assert_eq!(expected.trim(), generated) + } + } + } + + // Test the end to end trip: "net config -> wicked -> serialized XML" + #[test] + fn net_config_to_interface_config() { + let net_config_str: &str = include_str!("../test_data/net_config/net_config.toml"); + let net_config: NetConfig = toml::from_str(&net_config_str).unwrap(); + + for (name, config) in net_config.interfaces { + let mut path = test_data().join(&name.to_string()); + path.set_extension("xml"); + let expected = fs::read_to_string(path).unwrap(); + + let interface = WickedInterface::from_config(name, config); + let generated = serde_xml_rs::to_string(&interface).unwrap(); + + assert_eq!(expected.trim(), generated) + } + } +} diff --git a/sources/api/netdog/test_data/cmdline/multiple_interfaces b/sources/api/netdog/test_data/cmdline/multiple_interfaces new file mode 100644 index 00000000000..ba29014bfbc --- /dev/null +++ b/sources/api/netdog/test_data/cmdline/multiple_interfaces @@ -0,0 +1 @@ +BOOT_IMAGE=(hd0,gpt3)/vmlinuz root=/dev/dm-0 console=tty0 console=ttyS0,115200n8 netdog.default-interface=eth0:dhcp4,dhcp6? netdog.default-interface=eno1:dhcp4 rootwait ro raid=noautodetect random.trust_cpu=on selinux=1 enforcing=1 systemd.log_target=journal-or-kmsg systemd.log_color=0 net.ifnames=0 biosdevname=0 dm_verity.max_bios=-1 dm_verity.dev_wait=1 "dm-mod.create=root,,,ro,0 1884160 verity 1 PARTUUID=e8427c85-ecea-442e-886d-2ea750a6a4d7/PARTNROFF=1 PARTUUID=e8427c85-ecea-442e-886d-2ea750a6a4d7/PARTNROFF=2 4096 4096 235520 1 sha256 b8af768b8a451617821258abbee2bd24001039b208267e338213fe4a71940da8 43ed65c595bc5cff7054045d14194bb58adfb49576234c8b27f8dfa6b588ec3d 1 restart_on_corruption" diff --git a/sources/api/netdog/test_data/cmdline/no_interfaces b/sources/api/netdog/test_data/cmdline/no_interfaces new file mode 100644 index 00000000000..6c2e09ce2ef --- /dev/null +++ b/sources/api/netdog/test_data/cmdline/no_interfaces @@ -0,0 +1 @@ +BOOT_IMAGE=(hd0,gpt3)/vmlinuz root=/dev/dm-0 console=tty0 console=ttyS0,115200n8 rootwait ro raid=noautodetect random.trust_cpu=on selinux=1 enforcing=1 systemd.log_target=journal-or-kmsg systemd.log_color=0 net.ifnames=0 biosdevname=0 dm_verity.max_bios=-1 dm_verity.dev_wait=1 "dm-mod.create=root,,,ro,0 1884160 verity 1 PARTUUID=e8427c85-ecea-442e-886d-2ea750a6a4d7/PARTNROFF=1 PARTUUID=e8427c85-ecea-442e-886d-2ea750a6a4d7/PARTNROFF=2 4096 4096 235520 1 sha256 b8af768b8a451617821258abbee2bd24001039b208267e338213fe4a71940da8 43ed65c595bc5cff7054045d14194bb58adfb49576234c8b27f8dfa6b588ec3d 1 restart_on_corruption" diff --git a/sources/api/netdog/test_data/cmdline/ok b/sources/api/netdog/test_data/cmdline/ok new file mode 100644 index 00000000000..02adcc735e3 --- /dev/null +++ b/sources/api/netdog/test_data/cmdline/ok @@ -0,0 +1,2 @@ +BOOT_IMAGE=(hd0,gpt3)/vmlinuz root=/dev/dm-0 console=tty0 console=ttyS0,115200n8 +netdog.default-interface=eth0:dhcp4,dhcp6? rootwait ro raid=noautodetect random.trust_cpu=on selinux=1 enforcing=1 systemd.log_target=journal-or-kmsg systemd.log_color=0 net.ifnames=0 biosdevname=0 dm_verity.max_bios=-1 dm_verity.dev_wait=1 "dm-mod.create=root,,,ro,0 1884160 verity 1 PARTUUID=e8427c85-ecea-442e-886d-2ea750a6a4d7/PARTNROFF=1 PARTUUID=e8427c85-ecea-442e-886d-2ea750a6a4d7/PARTNROFF=2 4096 4096 235520 1 sha256 b8af768b8a451617821258abbee2bd24001039b208267e338213fe4a71940da8 43ed65c595bc5cff7054045d14194bb58adfb49576234c8b27f8dfa6b588ec3d 1 restart_on_corruption" diff --git a/sources/api/netdog/test_data/net_config/bad_version.toml b/sources/api/netdog/test_data/net_config/bad_version.toml new file mode 100644 index 00000000000..04b267d4cab --- /dev/null +++ b/sources/api/netdog/test_data/net_config/bad_version.toml @@ -0,0 +1,26 @@ +version = 50 + +[eno1] +dhcp4 = true + +[eno2] +dhcp6 = true + +[eno3] +dhcp4 = true +dhcp6 = false + +[eno4] +dhcp4 = false +dhcp6 = true + +[eno5] +dhcp4 = true +dhcp6 = true + +[eno6.dhcp4] +enabled = true +route-metric = 100 + +[eno6] +dhcp6 = false diff --git a/sources/api/netdog/test_data/net_config/invalid_dhcp_config.toml b/sources/api/netdog/test_data/net_config/invalid_dhcp_config.toml new file mode 100644 index 00000000000..48244a6b702 --- /dev/null +++ b/sources/api/netdog/test_data/net_config/invalid_dhcp_config.toml @@ -0,0 +1,3 @@ +version = 1 + +[eno1] diff --git a/sources/api/netdog/test_data/net_config/multiple_primary.toml b/sources/api/netdog/test_data/net_config/multiple_primary.toml new file mode 100644 index 00000000000..d13716cf180 --- /dev/null +++ b/sources/api/netdog/test_data/net_config/multiple_primary.toml @@ -0,0 +1,28 @@ +version = 1 + +[eno1] +dhcp4 = true +primary = true + +[eno2] +dhcp6 = true +primary = true + +[eno3] +dhcp4 = true +dhcp6 = false + +[eno4] +dhcp4 = false +dhcp6 = true + +[eno5] +dhcp4 = true +dhcp6 = true + +[eno6.dhcp4] +enabled = true +route-metric = 100 + +[eno6] +dhcp6 = false diff --git a/sources/api/netdog/test_data/net_config/net_config.toml b/sources/api/netdog/test_data/net_config/net_config.toml new file mode 100644 index 00000000000..a2e3fd843fc --- /dev/null +++ b/sources/api/netdog/test_data/net_config/net_config.toml @@ -0,0 +1,27 @@ +version = 1 + +[eno1] +dhcp4 = true + +[eno2] +dhcp6 = true +primary = true + +[eno3] +dhcp4 = true +dhcp6 = false + +[eno4] +dhcp4 = false +dhcp6 = true + +[eno5] +dhcp4 = true +dhcp6 = true + +[eno6.dhcp4] +enabled = true +route-metric = 100 + +[eno6] +dhcp6 = false diff --git a/sources/api/netdog/test_data/net_config/no_interfaces.toml b/sources/api/netdog/test_data/net_config/no_interfaces.toml new file mode 100644 index 00000000000..d9914dfa617 --- /dev/null +++ b/sources/api/netdog/test_data/net_config/no_interfaces.toml @@ -0,0 +1 @@ +version = 1 diff --git a/sources/api/netdog/test_data/net_config/no_primary.toml b/sources/api/netdog/test_data/net_config/no_primary.toml new file mode 100644 index 00000000000..c5e84037b5d --- /dev/null +++ b/sources/api/netdog/test_data/net_config/no_primary.toml @@ -0,0 +1,26 @@ +version = 1 + +[eno3] +dhcp4 = true +dhcp6 = false + +[eno2] +dhcp6 = true + +[eno1] +dhcp4 = true + +[eno4] +dhcp4 = false +dhcp6 = true + +[eno5] +dhcp4 = true +dhcp6 = true + +[eno6.dhcp4] +enabled = true +route-metric = 100 + +[eno6] +dhcp6 = false diff --git a/sources/api/netdog/test_data/wicked/eno1.xml b/sources/api/netdog/test_data/wicked/eno1.xml new file mode 100644 index 00000000000..871375a9e78 --- /dev/null +++ b/sources/api/netdog/test_data/wicked/eno1.xml @@ -0,0 +1 @@ +eno1boottrue diff --git a/sources/api/netdog/test_data/wicked/eno10.xml b/sources/api/netdog/test_data/wicked/eno10.xml new file mode 100644 index 00000000000..c69f7b70250 --- /dev/null +++ b/sources/api/netdog/test_data/wicked/eno10.xml @@ -0,0 +1 @@ +eno10boottrue1 diff --git a/sources/api/netdog/test_data/wicked/eno2.xml b/sources/api/netdog/test_data/wicked/eno2.xml new file mode 100644 index 00000000000..c36f034711a --- /dev/null +++ b/sources/api/netdog/test_data/wicked/eno2.xml @@ -0,0 +1 @@ +eno2boottrue diff --git a/sources/api/netdog/test_data/wicked/eno3.xml b/sources/api/netdog/test_data/wicked/eno3.xml new file mode 100644 index 00000000000..e9ef662b6ce --- /dev/null +++ b/sources/api/netdog/test_data/wicked/eno3.xml @@ -0,0 +1 @@ +eno3boottruefalse diff --git a/sources/api/netdog/test_data/wicked/eno4.xml b/sources/api/netdog/test_data/wicked/eno4.xml new file mode 100644 index 00000000000..594914e893c --- /dev/null +++ b/sources/api/netdog/test_data/wicked/eno4.xml @@ -0,0 +1 @@ +eno4bootfalsetrue diff --git a/sources/api/netdog/test_data/wicked/eno5.xml b/sources/api/netdog/test_data/wicked/eno5.xml new file mode 100644 index 00000000000..c1de2f1caea --- /dev/null +++ b/sources/api/netdog/test_data/wicked/eno5.xml @@ -0,0 +1 @@ +eno5boottruetrue diff --git a/sources/api/netdog/test_data/wicked/eno6.xml b/sources/api/netdog/test_data/wicked/eno6.xml new file mode 100644 index 00000000000..69bfb8dd72b --- /dev/null +++ b/sources/api/netdog/test_data/wicked/eno6.xml @@ -0,0 +1 @@ +eno6boottrue100false diff --git a/sources/api/netdog/test_data/wicked/eno7.xml b/sources/api/netdog/test_data/wicked/eno7.xml new file mode 100644 index 00000000000..672bb69bf7d --- /dev/null +++ b/sources/api/netdog/test_data/wicked/eno7.xml @@ -0,0 +1 @@ +eno7boottruetrue1 diff --git a/sources/api/netdog/test_data/wicked/eno8.xml b/sources/api/netdog/test_data/wicked/eno8.xml new file mode 100644 index 00000000000..b7811d72f39 --- /dev/null +++ b/sources/api/netdog/test_data/wicked/eno8.xml @@ -0,0 +1 @@ +eno8boottrue1true1 diff --git a/sources/api/netdog/test_data/wicked/eno9.xml b/sources/api/netdog/test_data/wicked/eno9.xml new file mode 100644 index 00000000000..b421b25b996 --- /dev/null +++ b/sources/api/netdog/test_data/wicked/eno9.xml @@ -0,0 +1 @@ +eno9boottrue1 From f68eec844c76f18cb9a80b9de9ac0810b6f5348e Mon Sep 17 00:00:00 2001 From: Zac Mrowicki Date: Tue, 12 Apr 2022 16:31:35 +0000 Subject: [PATCH 2/2] Use generated network configuration by default This commit removes the hard-coded `eth0.xml` network configuration file in favor of using `netdog` to generate the network configuration for all variants. It adds a new systemd unit file to run `netdog generate-net-config` early in boot, before the network is up. To generate the network configuration for AWS/VMware variants, we pass `netdog.default-interface=eth0:dhcp4,dhcp6?` which `netdog` interprets and generates network config similar to the hardcoded file previously used for these variants. For metal variants, we decided to use systemd-udevd's predictable device naming so we can count on network devices being named identically every boot. We currently pass `net.ifnames=0` on the kernel command line which disables predictable naming, which is fine for AWS and VMware variants as hardware is controlled and instances typically initially come up with a single interface in the same PCIe location. In order to continue using `net.ifnames=0` for AWS/VMware, we move this parameter out of the default kernel command line to the KERNEL_PARAMETERS section of each variants `Cargo.toml`. Aside: We previously passed `biosdevname=0` on the kernel command line as well. `biosdevname` is a udev helper utility written by Dell for consistent device naming based on SMBIOS info. We don't currently package the helper or include the udev rule that uses it, so we have removed the `biosdevname` parameter entirely. --- packages/os/generate-network-config.service | 15 +++++++++++ packages/os/os.spec | 4 ++- packages/release/eth0.xml | 28 --------------------- packages/release/release-tmpfiles.conf | 1 - packages/release/release.spec | 5 ---- packages/wicked/wicked-tmpfiles.conf | 1 + tools/rpm2img | 3 +-- variants/aws-dev/Cargo.toml | 4 ++- variants/aws-ecs-1/Cargo.toml | 2 ++ variants/aws-k8s-1.19/Cargo.toml | 2 ++ variants/aws-k8s-1.20/Cargo.toml | 2 ++ variants/aws-k8s-1.21-nvidia/Cargo.toml | 2 ++ variants/aws-k8s-1.21/Cargo.toml | 2 ++ variants/aws-k8s-1.22-nvidia/Cargo.toml | 2 ++ variants/aws-k8s-1.22/Cargo.toml | 2 ++ variants/vmware-dev/Cargo.toml | 4 ++- variants/vmware-k8s-1.20/Cargo.toml | 4 ++- variants/vmware-k8s-1.21/Cargo.toml | 4 ++- variants/vmware-k8s-1.22/Cargo.toml | 4 ++- 19 files changed, 49 insertions(+), 42 deletions(-) create mode 100644 packages/os/generate-network-config.service delete mode 100644 packages/release/eth0.xml diff --git a/packages/os/generate-network-config.service b/packages/os/generate-network-config.service new file mode 100644 index 00000000000..77110075e5c --- /dev/null +++ b/packages/os/generate-network-config.service @@ -0,0 +1,15 @@ +[Unit] +Description=Generate network configuration +# Block manual interactions with this service, since it could leave the system in an +# unexpected state +RefuseManualStart=true +RefuseManualStop=true + +[Service] +Type=oneshot +ExecStart=/usr/bin/netdog generate-net-config +RemainAfterExit=true +StandardError=journal+console + +[Install] +RequiredBy=network-pre.target diff --git a/packages/os/os.spec b/packages/os/os.spec index 11114db1a7c..f3c651fbe00 100644 --- a/packages/os/os.spec +++ b/packages/os/os.spec @@ -45,6 +45,7 @@ Source114: bootstrap-containers@.service Source115: link-kernel-modules.service Source116: load-kernel-modules.service Source117: cfsignal.service +Source118: generate-network-config.service # 2xx sources: tmpfilesd configs Source200: migration-tmpfiles.conf @@ -444,7 +445,7 @@ install -d %{buildroot}%{_cross_unitdir} install -p -m 0644 \ %{S:100} %{S:101} %{S:102} %{S:103} %{S:105} \ %{S:106} %{S:107} %{S:110} %{S:111} %{S:112} \ - %{S:113} %{S:114} \ + %{S:113} %{S:114} %{S:118} \ %if %{_is_vendor_variant} %{S:115} %{S:116} \ %endif @@ -487,6 +488,7 @@ install -p -m 0644 %{S:300} %{buildroot}%{_cross_udevrulesdir}/80-ephemeral-stor %files -n %{_cross_os}netdog %{_cross_bindir}/netdog %{_cross_tmpfilesdir}/netdog.conf +%{_cross_unitdir}/generate-network-config.service %files -n %{_cross_os}corndog %{_cross_bindir}/corndog diff --git a/packages/release/eth0.xml b/packages/release/eth0.xml deleted file mode 100644 index 06ae9ffa73a..00000000000 --- a/packages/release/eth0.xml +++ /dev/null @@ -1,28 +0,0 @@ - - eth0 - Primary Ethernet Device - - - boot - - - - - - - false - false - - - - true - - - - true - 1 - - - - - diff --git a/packages/release/release-tmpfiles.conf b/packages/release/release-tmpfiles.conf index 0e3aec8d2a1..8d015f348a4 100644 --- a/packages/release/release-tmpfiles.conf +++ b/packages/release/release-tmpfiles.conf @@ -1,5 +1,4 @@ C /etc/nsswitch.conf - - - - -C /etc/wicked/ifconfig/eth0.xml - - - - d /var/log/kdump 0700 root root - d /sys/fs/cgroup/cpuset/runtime.slice 0755 root root - d /sys/fs/cgroup/hugetlb/runtime.slice 0755 root root - diff --git a/packages/release/release.spec b/packages/release/release.spec index 9d0bdc0ed11..ec8d8d1cdb1 100644 --- a/packages/release/release.spec +++ b/packages/release/release.spec @@ -18,7 +18,6 @@ Source201: proxy-env Source202: hostname-env Source203: hosts.template -Source1000: eth0.xml Source1001: multi-user.target Source1002: configured.target Source1003: preconfigured.target @@ -106,9 +105,6 @@ Requires: %{_cross_os}wicked install -d %{buildroot}%{_cross_factorydir}%{_cross_sysconfdir} install -p -m 0644 %{S:11} %{buildroot}%{_cross_factorydir}%{_cross_sysconfdir} -install -d %{buildroot}%{_cross_factorydir}%{_cross_sysconfdir}/wicked/ifconfig -install -p -m 0644 %{S:1000} %{buildroot}%{_cross_factorydir}%{_cross_sysconfdir}/wicked/ifconfig - install -d %{buildroot}%{_cross_libdir}/repart.d install -p -m 0644 %{S:96} %{buildroot}%{_cross_libdir}/repart.d/80-local.conf @@ -172,7 +168,6 @@ ln -s preconfigured.target %{buildroot}%{_cross_unitdir}/default.target %files %{_cross_factorydir}%{_cross_sysconfdir}/nsswitch.conf -%{_cross_factorydir}%{_cross_sysconfdir}/wicked/ifconfig/eth0.xml %{_cross_sysctldir}/80-release.conf %{_cross_tmpfilesdir}/release.conf %{_cross_libdir}/os-release diff --git a/packages/wicked/wicked-tmpfiles.conf b/packages/wicked/wicked-tmpfiles.conf index 03ba2229af1..7666b323df1 100644 --- a/packages/wicked/wicked-tmpfiles.conf +++ b/packages/wicked/wicked-tmpfiles.conf @@ -2,6 +2,7 @@ C /etc/wicked/client.xml - - - - C /etc/wicked/common.xml - - - - C /etc/wicked/nanny.xml - - - - C /etc/wicked/server.xml - - - - +d /etc/wicked/ifconfig 0700 root root - d /var/lib/wicked 0700 root root - Z /var/lib/wicked 0700 root root - diff --git a/tools/rpm2img b/tools/rpm2img index b8af8e0483c..8eb4d69fe93 100755 --- a/tools/rpm2img +++ b/tools/rpm2img @@ -280,8 +280,7 @@ menuentry "${PRETTY_NAME} ${VERSION_ID}" { dm-mod.create="root,,,ro,0 $VERITY_DATA_512B_BLOCKS verity $VERITY_VERSION PARTUUID=\$boot_uuid/PARTNROFF=1 PARTUUID=\$boot_uuid/PARTNROFF=2 \\ $VERITY_DATA_BLOCK_SIZE $VERITY_HASH_BLOCK_SIZE $VERITY_DATA_4K_BLOCKS 1 $VERITY_HASH_ALGORITHM $VERITY_ROOT_HASH $VERITY_SALT 1 restart_on_corruption" \\ -- \\ - systemd.log_target=journal-or-kmsg systemd.log_color=0 \\ - net.ifnames=0 biosdevname=0 + systemd.log_target=journal-or-kmsg systemd.log_color=0 ${INITRD} } EOF diff --git a/variants/aws-dev/Cargo.toml b/variants/aws-dev/Cargo.toml index a8e298785c2..12294964bda 100644 --- a/variants/aws-dev/Cargo.toml +++ b/variants/aws-dev/Cargo.toml @@ -12,7 +12,9 @@ kernel-parameters = [ "console=tty0", "console=ttyS0,115200n8", # Only reserve if there are at least 2GB - "crashkernel=2G-:256M" + "crashkernel=2G-:256M", + "net.ifnames=0", + "netdog.default-interface=eth0:dhcp4,dhcp6?", ] included-packages = [ # core diff --git a/variants/aws-ecs-1/Cargo.toml b/variants/aws-ecs-1/Cargo.toml index 152126ea44f..35d71ca3c5a 100644 --- a/variants/aws-ecs-1/Cargo.toml +++ b/variants/aws-ecs-1/Cargo.toml @@ -9,6 +9,8 @@ build = "build.rs" kernel-parameters = [ "console=tty0", "console=ttyS0,115200n8", + "net.ifnames=0", + "netdog.default-interface=eth0:dhcp4,dhcp6?", ] included-packages = [ # core diff --git a/variants/aws-k8s-1.19/Cargo.toml b/variants/aws-k8s-1.19/Cargo.toml index 9d2b2de70ec..2ff1859fbca 100644 --- a/variants/aws-k8s-1.19/Cargo.toml +++ b/variants/aws-k8s-1.19/Cargo.toml @@ -13,6 +13,8 @@ exclude = ["README.md"] kernel-parameters = [ "console=tty0", "console=ttyS0,115200n8", + "net.ifnames=0", + "netdog.default-interface=eth0:dhcp4,dhcp6?", ] included-packages = [ "aws-iam-authenticator", diff --git a/variants/aws-k8s-1.20/Cargo.toml b/variants/aws-k8s-1.20/Cargo.toml index dce051e7134..b9b9a9e2f21 100644 --- a/variants/aws-k8s-1.20/Cargo.toml +++ b/variants/aws-k8s-1.20/Cargo.toml @@ -21,6 +21,8 @@ included-packages = [ kernel-parameters = [ "console=tty0", "console=ttyS0,115200n8", + "net.ifnames=0", + "netdog.default-interface=eth0:dhcp4,dhcp6?", ] [lib] diff --git a/variants/aws-k8s-1.21-nvidia/Cargo.toml b/variants/aws-k8s-1.21-nvidia/Cargo.toml index 125ec2849da..6d288e0f8bb 100644 --- a/variants/aws-k8s-1.21-nvidia/Cargo.toml +++ b/variants/aws-k8s-1.21-nvidia/Cargo.toml @@ -26,6 +26,8 @@ included-packages = [ kernel-parameters = [ "console=tty0", "console=ttyS0,115200n8", + "net.ifnames=0", + "netdog.default-interface=eth0:dhcp4,dhcp6?", ] [lib] diff --git a/variants/aws-k8s-1.21/Cargo.toml b/variants/aws-k8s-1.21/Cargo.toml index efb6ef04de8..eaa266ab1d0 100644 --- a/variants/aws-k8s-1.21/Cargo.toml +++ b/variants/aws-k8s-1.21/Cargo.toml @@ -21,6 +21,8 @@ included-packages = [ kernel-parameters = [ "console=tty0", "console=ttyS0,115200n8", + "net.ifnames=0", + "netdog.default-interface=eth0:dhcp4,dhcp6?", ] [lib] diff --git a/variants/aws-k8s-1.22-nvidia/Cargo.toml b/variants/aws-k8s-1.22-nvidia/Cargo.toml index 15d9f6f42bc..aed7c3aa9b6 100644 --- a/variants/aws-k8s-1.22-nvidia/Cargo.toml +++ b/variants/aws-k8s-1.22-nvidia/Cargo.toml @@ -26,6 +26,8 @@ included-packages = [ kernel-parameters = [ "console=tty0", "console=ttyS0,115200n8", + "net.ifnames=0", + "netdog.default-interface=eth0:dhcp4,dhcp6?", ] [lib] diff --git a/variants/aws-k8s-1.22/Cargo.toml b/variants/aws-k8s-1.22/Cargo.toml index a72adbbe9a3..c8376d4590a 100644 --- a/variants/aws-k8s-1.22/Cargo.toml +++ b/variants/aws-k8s-1.22/Cargo.toml @@ -21,6 +21,8 @@ included-packages = [ kernel-parameters = [ "console=tty0", "console=ttyS0,115200n8", + "net.ifnames=0", + "netdog.default-interface=eth0:dhcp4,dhcp6?", ] [lib] diff --git a/variants/vmware-dev/Cargo.toml b/variants/vmware-dev/Cargo.toml index 51e0ca667be..36cc7db1c0f 100644 --- a/variants/vmware-dev/Cargo.toml +++ b/variants/vmware-dev/Cargo.toml @@ -14,7 +14,9 @@ kernel-parameters = [ "console=ttyS0,115200n8", "console=tty1", # Only reserve if there are at least 2GB - "crashkernel=2G-:256M" + "crashkernel=2G-:256M", + "net.ifnames=0", + "netdog.default-interface=eth0:dhcp4,dhcp6?", ] included-packages = [ # core diff --git a/variants/vmware-k8s-1.20/Cargo.toml b/variants/vmware-k8s-1.20/Cargo.toml index 774b9460128..c3c9eac57dd 100644 --- a/variants/vmware-k8s-1.20/Cargo.toml +++ b/variants/vmware-k8s-1.20/Cargo.toml @@ -16,7 +16,9 @@ kernel-parameters = [ "console=ttyS0,115200n8", "console=tty1", # Only reserve if there are at least 2GB - "crashkernel=2G-:256M" + "crashkernel=2G-:256M", + "net.ifnames=0", + "netdog.default-interface=eth0:dhcp4,dhcp6?", ] included-packages = [ "cni", diff --git a/variants/vmware-k8s-1.21/Cargo.toml b/variants/vmware-k8s-1.21/Cargo.toml index 732f4d1cbbc..a0a196c2e38 100644 --- a/variants/vmware-k8s-1.21/Cargo.toml +++ b/variants/vmware-k8s-1.21/Cargo.toml @@ -16,7 +16,9 @@ kernel-parameters = [ "console=ttyS0,115200n8", "console=tty1", # Only reserve if there are at least 2GB - "crashkernel=2G-:256M" + "crashkernel=2G-:256M", + "net.ifnames=0", + "netdog.default-interface=eth0:dhcp4,dhcp6?", ] included-packages = [ "cni", diff --git a/variants/vmware-k8s-1.22/Cargo.toml b/variants/vmware-k8s-1.22/Cargo.toml index f1f107f2339..0050f9e4c48 100644 --- a/variants/vmware-k8s-1.22/Cargo.toml +++ b/variants/vmware-k8s-1.22/Cargo.toml @@ -19,7 +19,9 @@ kernel-parameters = [ "console=ttyS0,115200n8", "console=tty1", # Only reserve if there are at least 2GB - "crashkernel=2G-:256M" + "crashkernel=2G-:256M", + "net.ifnames=0", + "netdog.default-interface=eth0:dhcp4,dhcp6?", ] included-packages = [ "cni",