diff --git a/PROVISIONING-METAL.md b/PROVISIONING-METAL.md index 33427d52ded..ae9299769aa 100644 --- a/PROVISIONING-METAL.md +++ b/PROVISIONING-METAL.md @@ -76,7 +76,7 @@ When these services fail, your machine will not connect to any cluster and will #### `net.toml` structure The configuration file must be valid TOML and have the filename `net.toml`. -The first and required top level key in the file is `version`, currently only `1` is supported. +The first and required top level key in the file is `version`; the latest is version `2`. The rest of the file is a map of interface name to supported settings. Interface names are expected to be correct as per `udevd` naming, no interface naming or matching is supported. (See the note below regarding `udevd` interface naming.) @@ -92,9 +92,22 @@ Interface names are expected to be correct as per `udevd` naming, no interface n * `enabled` (boolean, required): Enables DHCP6. * `optional` (boolean): the system will request a lease using this protocol, but will not wait for a valid lease to consider this interface configured. +As of version `2` static addressing with simple routes is supported via the below settings. +Please keep in mind that when using static addresses, DNS information must be supplied to the system via user data: [`settings.dns`](https://github.com/bottlerocket-os/bottlerocket#network-settings). +* `static4` (map): IPv4 static address settings. + * `addresses` (list of quoted IPv4 address including prefix): The desired IPv4 IP addresses, including prefix i.e. `["192.168.14.2/24"]`. The first IP in the list will be used as the primary IP which `kubelet` will use when joining the cluster. If IPv4 and IPv6 static addresses exist, the first IPv4 address is used. +* `static6` (map): IPv6 static address settings. + * `addresses` (list of quoted IPv6 address including prefix): The desired IPv6 IP addresses, including prefix i.e. `["2001:dead:beef::2/64"]`. The first IP in the list will be used as the primary IP which `kubelet` will use when joining the cluster. If IPv4 and IPv6 static addresses exist, the first IPv4 address is used. + +* `route` (map): Static route; multiple routes can be added. (cannot be used in conjuction with DHCP) + * `to` (`"default"` or IP address with prefix, required): Destination address. + * `from` (IP address): Source IP address. + * `via` (IP address): Gateway IP address. If no gateway is provided, a scope of `link` is assumed. + * `route-metric` (integer): Relative route priority. + Example `net.toml` with comments: ```toml -version = 1 +version = 2 # "eno1" is the interface name [eno1] @@ -108,12 +121,35 @@ primary = true # `enabled` is a boolean and is a required key when # setting up DHCP this way enabled = true -# Route metric may be supplied for ipv4 +# Route metric may be supplied for IPv4 route-metric = 200 [eno2.dhcp6] enabled = true optional = true + +[eno3.static4] +addresses = ["10.0.0.10/24", "11.0.0.11/24"] + +# Multiple routes may be configured +[[eno3.route]] +to = "default" +via = "10.0.0.1" +route-metric = 100 + +[[eno3.route]] +to = "default" +via = "11.0.0.1" +route-metric = 200 + +[eno4.static4] +addresses = ["192.168.14.5/24"] + +# Using a source IP and non-default route +[[eno4.route]] +to = "10.10.10.0/24" +from = "192.168.14.5" +via = "192.168.14.25" ``` **An additional note on network device names** diff --git a/sources/api/netdog/src/cli/install.rs b/sources/api/netdog/src/cli/install.rs index 02fe3490668..6e5b9d9cdb7 100644 --- a/sources/api/netdog/src/cli/install.rs +++ b/sources/api/netdog/src/cli/install.rs @@ -1,12 +1,12 @@ use super::{error, InterfaceFamily, InterfaceType, Result}; use crate::dns::DnsSettings; -use crate::lease::{lease_path, LeaseInfo}; +use crate::lease::{dhcp_lease_path, static_lease_path, LeaseInfo}; use crate::{CURRENT_IP, PRIMARY_INTERFACE}; use argh::FromArgs; -use snafu::{OptionExt, ResultExt}; +use snafu::{ensure, OptionExt, ResultExt}; use std::fs; use std::net::IpAddr; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; #[derive(FromArgs, PartialEq, Debug)] #[argh(subcommand, name = "install")] @@ -50,36 +50,57 @@ pub(crate) fn run(args: InstallArgs) -> Result<()> { } match (&args.interface_type, &args.interface_family) { - (InterfaceType::Dhcp, InterfaceFamily::Ipv4 | InterfaceFamily::Ipv6) => { - // A lease should exist when using DHCP - let primary_lease_path = - lease_path(&primary_interface).context(error::MissingLeaseSnafu { - interface: primary_interface, - })?; - if args.data_file != primary_lease_path { - return error::PrimaryLeaseConflictSnafu { - wicked_path: args.data_file, - generated_path: primary_lease_path, - } - .fail(); - } - - // Use DNS API settings if they exist, supplementing any missing settings with settings - // derived from the primary interface's DHCP lease - let lease = - LeaseInfo::from_lease(primary_lease_path).context(error::LeaseParseFailedSnafu)?; - let dns_settings = DnsSettings::from_config_or_lease(Some(&lease)) - .context(error::GetDnsSettingsSnafu)?; - dns_settings - .write_resolv_conf() - .context(error::ResolvConfWriteFailedSnafu)?; - + ( + interface_type @ (InterfaceType::Dhcp | InterfaceType::Static), + InterfaceFamily::Ipv4 | InterfaceFamily::Ipv6, + ) => { + let lease = fetch_lease(primary_interface, interface_type, args.data_file)?; + write_resolv_conf(&lease)?; write_current_ip(&lease.ip_address.addr())?; } } Ok(()) } +/// Given an interface, its type, and wicked's known location of the lease, compare our known lease +/// location, parse and return a LeaseInfo. +fn fetch_lease( + interface: S, + interface_type: &InterfaceType, + data_file: P, +) -> Result +where + S: AsRef, + P: AsRef, +{ + let interface = interface.as_ref(); + let data_file = data_file.as_ref(); + let lease_path = match interface_type { + InterfaceType::Dhcp => dhcp_lease_path(interface), + InterfaceType::Static => static_lease_path(interface), + } + .context(error::MissingLeaseSnafu { interface })?; + + ensure!( + data_file == lease_path, + error::PrimaryLeaseConflictSnafu { + wicked_path: data_file, + generated_path: lease_path, + } + ); + + LeaseInfo::from_lease(&lease_path).context(error::LeaseParseFailedSnafu) +} + +/// Given a lease, fetch DNS settings from the lease and/or config and write the resolv.conf +fn write_resolv_conf(lease: &LeaseInfo) -> Result<()> { + let dns_settings = + DnsSettings::from_config_or_lease(Some(lease)).context(error::GetDnsSettingsSnafu)?; + dns_settings + .write_resolv_conf() + .context(error::ResolvConfWriteFailedSnafu) +} + /// Persist the current IP address to file fn write_current_ip(ip: &IpAddr) -> Result<()> { fs::write(CURRENT_IP, ip.to_string()) diff --git a/sources/api/netdog/src/cli/mod.rs b/sources/api/netdog/src/cli/mod.rs index b43560ac1b5..de8709ffb1c 100644 --- a/sources/api/netdog/src/cli/mod.rs +++ b/sources/api/netdog/src/cli/mod.rs @@ -22,6 +22,7 @@ pub(crate) use write_resolv_conf::WriteResolvConfArgs; #[serde(rename_all = "kebab-case")] enum InterfaceType { Dhcp, + Static, } #[derive(Debug, PartialEq, Deserialize)] diff --git a/sources/api/netdog/src/cli/write_resolv_conf.rs b/sources/api/netdog/src/cli/write_resolv_conf.rs index f15208f31e7..552c2ca0b01 100644 --- a/sources/api/netdog/src/cli/write_resolv_conf.rs +++ b/sources/api/netdog/src/cli/write_resolv_conf.rs @@ -1,6 +1,6 @@ use super::{error, Result}; use crate::dns::DnsSettings; -use crate::lease::{lease_path, LeaseInfo}; +use crate::lease::{dhcp_lease_path, LeaseInfo}; use crate::PRIMARY_INTERFACE; use argh::FromArgs; use snafu::ResultExt; @@ -12,8 +12,9 @@ use std::fs; pub(crate) struct WriteResolvConfArgs {} pub(crate) fn run() -> Result<()> { - // Use DNS API settings if they exist, supplementing any missing settings with settings - // derived from the primary interface's DHCP lease if it exists + // Use DNS API settings if they exist, supplementing any missing settings with settings derived + // from the primary interface's DHCP lease if it exists. Static leases don't contain any DNS + // data, so don't bother looking there. let primary_interface = fs::read_to_string(PRIMARY_INTERFACE) .context(error::PrimaryInterfaceReadSnafu { path: PRIMARY_INTERFACE, @@ -21,7 +22,7 @@ pub(crate) fn run() -> Result<()> { .trim() .to_lowercase(); - let primary_lease_path = lease_path(&primary_interface); + let primary_lease_path = dhcp_lease_path(&primary_interface); let dns_settings = if let Some(primary_lease_path) = primary_lease_path { let lease = LeaseInfo::from_lease(&primary_lease_path).context(error::LeaseParseFailedSnafu)?; diff --git a/sources/api/netdog/src/dns.rs b/sources/api/netdog/src/dns.rs index c76c7d88e14..10ccc5c3ffe 100644 --- a/sources/api/netdog/src/dns.rs +++ b/sources/api/netdog/src/dns.rs @@ -37,7 +37,7 @@ impl DnsSettings { /// Merge missing DNS settings into `self` using DHCP lease fn merge_lease(&mut self, lease: &LeaseInfo) { if self.nameservers.is_none() { - self.nameservers = Some(lease.dns_servers.clone()); + self.nameservers = lease.dns_servers.clone(); } if self.search.is_none() { diff --git a/sources/api/netdog/src/lease.rs b/sources/api/netdog/src/lease.rs index 81c4a0eb080..36772b93cb5 100644 --- a/sources/api/netdog/src/lease.rs +++ b/sources/api/netdog/src/lease.rs @@ -21,10 +21,13 @@ lazy_static! { #[derive(Debug, Deserialize)] #[allow(dead_code)] pub(crate) struct LeaseInfo { + // When multiple IP addresses exist for an interface, the second address's key in the lease + // file will be `IPADDR_1`, `IPADDR_2`, and so on. Parsing the lease for "ipaddr" means we + // will always pick up the first configured IP address. #[serde(rename = "ipaddr")] pub(crate) ip_address: IpNet, #[serde(rename = "dnsservers")] - pub(crate) dns_servers: BTreeSet, + pub(crate) dns_servers: Option>, #[serde(rename = "dnsdomain")] pub(crate) dns_domain: Option, #[serde(rename = "dnssearch")] @@ -63,15 +66,36 @@ impl LeaseInfo { } } -/// Return the path to a given interface's ipv4/ipv6 lease if it exists, favoring ipv4 if both +/// Return the path to a given interface's DHCP ipv4/ipv6 lease if it exists, favoring ipv4 if both /// ipv4 and ipv6 exist -pub(crate) fn lease_path(interface: S) -> Option +pub(crate) fn dhcp_lease_path(interface: S) -> Option where S: AsRef, { + get_lease_path("dhcp", interface) +} + +/// Return the path to a given interface's static ipv4/ipv6 lease if it exists, favoring ipv4 if +/// both ipv4 and ipv6 exist +pub(crate) fn static_lease_path(interface: S) -> Option +where + S: AsRef, +{ + get_lease_path("static", interface) +} + +/// Given a lease type and interface, return the path to the ipv4/6 lease file if it exists, +/// favoring ipv4 if both ipv4 and ipv6 exist +fn get_lease_path(lease_type: S1, interface: S2) -> Option +where + S1: AsRef, + S2: AsRef, +{ + let lease_type = lease_type.as_ref(); let interface = interface.as_ref(); - let ipv4 = Path::new(LEASE_DIR).join(format!("leaseinfo.{}.dhcp.ipv4", interface)); - let ipv6 = Path::new(LEASE_DIR).join(format!("leaseinfo.{}.dhcp.ipv6", interface)); + + let ipv4 = Path::new(LEASE_DIR).join(format!("leaseinfo.{}.{}.ipv4", interface, lease_type)); + let ipv6 = Path::new(LEASE_DIR).join(format!("leaseinfo.{}.{}.ipv6", interface, lease_type)); // If both ipv4 and ipv6 leases exist, use the ipv4 lease for DNS settings let ipv4_exists = Path::exists(&ipv4); diff --git a/sources/api/netdog/src/net_config/mod.rs b/sources/api/netdog/src/net_config/mod.rs index c76f3f81c02..977203eebce 100644 --- a/sources/api/netdog/src/net_config/mod.rs +++ b/sources/api/netdog/src/net_config/mod.rs @@ -5,13 +5,16 @@ //! These structures are the user-facing options for configuring one or more network interfaces. mod dhcp; mod error; +mod static_address; mod v1; +mod v2; use crate::wicked::WickedInterface; pub(crate) use dhcp::{Dhcp4ConfigV1, Dhcp4OptionsV1, Dhcp6ConfigV1, Dhcp6OptionsV1}; pub(crate) use error::{Error, Result}; use serde::Deserialize; use snafu::{ensure, ResultExt}; +pub(crate) use static_address::{RouteTo, RouteV1, StaticConfigV1}; use std::fs; use std::path::Path; use std::str::FromStr; @@ -93,7 +96,8 @@ fn deserialize_config(config_str: &str) -> Result> { } = toml::from_str(config_str).context(error::NetConfigParseSnafu)?; let net_config: Box = match version { - 1 => validate_config::(interface_config)?, + 1 => validate_config::(interface_config)?, + 2 => validate_config::(interface_config)?, _ => { return error::InvalidNetConfigSnafu { reason: format!("Unknown network config version: {}", version), diff --git a/sources/api/netdog/src/net_config/static_address.rs b/sources/api/netdog/src/net_config/static_address.rs new file mode 100644 index 00000000000..9d4c7beceba --- /dev/null +++ b/sources/api/netdog/src/net_config/static_address.rs @@ -0,0 +1,65 @@ +use ipnet::IpNet; +use serde::Deserialize; +use snafu::ResultExt; +use std::collections::BTreeSet; +use std::convert::TryFrom; +use std::net::IpAddr; + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct StaticConfigV1 { + pub(crate) addresses: BTreeSet, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct RouteV1 { + pub(crate) to: RouteTo, + pub(crate) from: Option, + pub(crate) via: Option, + #[serde(rename = "route-metric")] + pub(crate) route_metric: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(try_from = "String")] +pub(crate) enum RouteTo { + DefaultRoute, + Ip(IpNet), +} + +// Allows the user to pass the string "default" or a valid ip address prefix. We can't use an +// untagged enum for this (#[serde(untagged)]) because "default" directly maps to one of the +// variants. Serde will only allow the "untagged" attribute if neither variant directly matches. +impl TryFrom for RouteTo { + type Error = error::Error; + + fn try_from(input: String) -> Result { + let input = input.to_lowercase(); + Ok(match input.as_str() { + "default" => RouteTo::DefaultRoute, + _ => { + let ip: IpNet = input + .parse() + .context(error::InvalidRouteDestinationSnafu { input })?; + RouteTo::Ip(ip) + } + }) + } +} + +mod error { + use snafu::Snafu; + + #[derive(Debug, Snafu)] + #[snafu(visibility(pub(crate)))] + pub(crate) enum Error { + #[snafu(display("Invalid route destination, must be 'default' or a valid IP address prefix. Received '{}': {}", input, source))] + InvalidRouteDestination { + input: String, + source: ipnet::AddrParseError, + }, + } +} + +type Result = std::result::Result; diff --git a/sources/api/netdog/src/net_config/test_macros/mod.rs b/sources/api/netdog/src/net_config/test_macros/mod.rs index 782c7476a16..7677dfa02b6 100644 --- a/sources/api/netdog/src/net_config/test_macros/mod.rs +++ b/sources/api/netdog/src/net_config/test_macros/mod.rs @@ -15,9 +15,12 @@ pub(super) mod basic; #[cfg(test)] pub(super) mod dhcp; +#[cfg(test)] +pub(super) mod static_address; pub(super) use basic::basic_tests; pub(super) use dhcp::dhcp_tests; +pub(super) use static_address::static_address_tests; /// gen_boilerplate!() is a convenience macro meant to be used inside of test macros to generate /// some generally useful boilerplate code. It creates a `VERSION` constant in case the test diff --git a/sources/api/netdog/src/net_config/test_macros/static_address.rs b/sources/api/netdog/src/net_config/test_macros/static_address.rs new file mode 100644 index 00000000000..0ad472f470e --- /dev/null +++ b/sources/api/netdog/src/net_config/test_macros/static_address.rs @@ -0,0 +1,67 @@ +macro_rules! static_address_tests { + ($version:expr) => { + mod static_address { + use $crate::net_config::deserialize_config; + use $crate::net_config::test_macros::gen_boilerplate; + + gen_boilerplate!($version, "static_address"); + + #[test] + fn ok_config() { + let ok = net_config().join("net_config.toml"); + let rendered = render_config_template(ok); + assert!(deserialize_config(&rendered).is_ok()) + } + + #[test] + fn dhcp_and_static_addresses() { + let ok = net_config().join("dhcp_and_static.toml"); + let rendered = render_config_template(ok); + assert!(deserialize_config(&rendered).is_ok()) + } + + #[test] + fn dhcp_and_routes() { + let bad = net_config().join("dhcp_and_routes.toml"); + let rendered = render_config_template(bad); + assert!(deserialize_config(&rendered).is_err()) + } + + #[test] + fn no_dhcp_or_static() { + let bad = net_config().join("no_dhcp_or_static.toml"); + let rendered = render_config_template(bad); + assert!(deserialize_config(&rendered).is_err()) + } + + #[test] + fn routes_no_addresses() { + let bad = net_config().join("routes_no_addresses.toml"); + let rendered = render_config_template(bad); + assert!(deserialize_config(&rendered).is_err()) + } + + #[test] + fn invalid_static_config() { + let bad = net_config().join("invalid_static_config.toml"); + let rendered = render_config_template(bad); + assert!(deserialize_config(&rendered).is_err()) + } + + #[test] + fn ipv6_in_static4() { + let bad = net_config().join("ipv6_in_static4.toml"); + let rendered = render_config_template(bad); + assert!(deserialize_config(&rendered).is_err()) + } + + #[test] + fn ipv4_in_static6() { + let bad = net_config().join("ipv4_in_static6.toml"); + let rendered = render_config_template(bad); + assert!(deserialize_config(&rendered).is_err()) + } + } + }; +} +pub(crate) use static_address_tests; diff --git a/sources/api/netdog/src/net_config/v1.rs b/sources/api/netdog/src/net_config/v1.rs index 1ecf7c08356..9b39ec37587 100644 --- a/sources/api/netdog/src/net_config/v1.rs +++ b/sources/api/netdog/src/net_config/v1.rs @@ -5,7 +5,7 @@ use super::{error, Dhcp4ConfigV1, Dhcp6ConfigV1, Error, Interfaces, Result, Vali use crate::{ interface_name::InterfaceName, net_config::{Dhcp4OptionsV1, Dhcp6OptionsV1}, - wicked::{WickedControl, WickedDhcp4, WickedDhcp6, WickedInterface}, + wicked::{WickedDhcp4, WickedDhcp6, WickedInterface}, }; use indexmap::indexmap; use indexmap::IndexMap; @@ -55,13 +55,11 @@ impl Interfaces for NetConfigV1 { for (name, config) in &self.interfaces { let wicked_dhcp4 = config.dhcp4.clone().map(WickedDhcp4::from); let wicked_dhcp6 = config.dhcp6.clone().map(WickedDhcp6::from); - let wicked_interface = WickedInterface { - name: name.clone(), - control: WickedControl::default(), - ipv4_dhcp: wicked_dhcp4, - ipv6_dhcp: wicked_dhcp6, - }; - wicked_interfaces.push(wicked_interface) + let mut interface = WickedInterface::new(name.clone()); + interface.ipv4_dhcp = wicked_dhcp4; + interface.ipv6_dhcp = wicked_dhcp6; + + wicked_interfaces.push(interface) } wicked_interfaces diff --git a/sources/api/netdog/src/net_config/v2.rs b/sources/api/netdog/src/net_config/v2.rs new file mode 100644 index 00000000000..1dd4965fd33 --- /dev/null +++ b/sources/api/netdog/src/net_config/v2.rs @@ -0,0 +1,145 @@ +//! The `v2` module contains the second version of the network configuration and implements the +//! appropriate traits. + +use super::static_address::{RouteV1, StaticConfigV1}; +use super::{error, Dhcp4ConfigV1, Dhcp6ConfigV1, Interfaces, Result, Validate}; +use crate::interface_name::InterfaceName; +use crate::wicked::{WickedDhcp4, WickedDhcp6, WickedInterface, WickedRoutes, WickedStaticAddress}; +use indexmap::IndexMap; +use ipnet::IpNet; +use serde::Deserialize; +use snafu::ensure; + +#[derive(Debug, Deserialize)] +pub(crate) struct NetConfigV2 { + #[serde(flatten)] + pub(crate) interfaces: IndexMap, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct NetInterfaceV2 { + // Use this interface as the primary interface for the system + pub(crate) primary: Option, + pub(crate) dhcp4: Option, + pub(crate) dhcp6: Option, + pub(crate) static4: Option, + pub(crate) static6: Option, + #[serde(rename = "route")] + pub(crate) routes: Option>, +} + +impl Interfaces for NetConfigV2 { + fn primary_interface(&self) -> Option { + self.interfaces + .iter() + .find(|(_, v)| v.primary == Some(true)) + .or_else(|| self.interfaces.first()) + .map(|(n, _)| n.to_string()) + } + + fn has_interfaces(&self) -> bool { + !self.interfaces.is_empty() + } + + fn as_wicked_interfaces(&self) -> Vec { + let mut wicked_interfaces = Vec::with_capacity(self.interfaces.len()); + for (name, config) in &self.interfaces { + let mut interface = WickedInterface::new(name.clone()); + interface.ipv4_dhcp = config.dhcp4.clone().map(WickedDhcp4::from); + interface.ipv6_dhcp = config.dhcp6.clone().map(WickedDhcp6::from); + + // Based on the existence of static addresses and routes, create the ipv4/6_static + // struct members. They must be `Option`s because we want to avoid serializing empty + // tags into the config file + let maybe_routes = config.routes.clone().map(WickedRoutes::from); + let maybe_ipv4_static = WickedStaticAddress::maybe_new( + config.static4.clone(), + maybe_routes.as_ref().and_then(|s| s.ipv4.clone()), + ); + let maybe_ipv6_static = WickedStaticAddress::maybe_new( + config.static6.clone(), + maybe_routes.as_ref().and_then(|s| s.ipv6.clone()), + ); + interface.ipv4_static = maybe_ipv4_static; + interface.ipv6_static = maybe_ipv6_static; + + wicked_interfaces.push(interface); + } + + wicked_interfaces + } +} + +impl Validate for NetConfigV2 { + fn validate(&self) -> Result<()> { + for (_name, config) in &self.interfaces { + let has_static = config.static4.is_some() || config.static6.is_some(); + let has_dhcp = config.dhcp4.is_some() || config.dhcp6.is_some(); + let has_routes = config.routes.is_some(); + + if !has_dhcp && !has_static { + return error::InvalidNetConfigSnafu { + reason: "each interface must configure dhcp and/or static addresses", + } + .fail(); + } + + // wicked doesn't support static routes with dhcp + if has_dhcp && has_routes { + return error::InvalidNetConfigSnafu { + reason: "static routes are not supported with dhcp", + } + .fail(); + } + + if has_routes && !has_static { + return error::InvalidNetConfigSnafu { + reason: "interfaces must set static addresses in order to use routes", + } + .fail(); + } + + if let Some(config) = &config.static4 { + ensure!( + config.addresses.iter().all(|a| matches!(a, IpNet::V4(_))), + error::InvalidNetConfigSnafu { + reason: "'static4' may only contain IPv4 addresses" + } + ) + } + + if let Some(config) = &config.static6 { + ensure!( + config.addresses.iter().all(|a| matches!(a, IpNet::V6(_))), + error::InvalidNetConfigSnafu { + reason: "'static6' may only contain IPv6 addresses" + } + ) + } + } + + let primary_count = self + .interfaces + .values() + .filter(|v| v.primary == Some(true)) + .count(); + ensure!( + primary_count <= 1, + error::InvalidNetConfigSnafu { + reason: "multiple primary interfaces defined, expected 1" + } + ); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::net_config::test_macros::{basic_tests, dhcp_tests, static_address_tests}; + + basic_tests!(2); + dhcp_tests!(2); + static_address_tests!(2); +} diff --git a/sources/api/netdog/src/wicked/dhcp.rs b/sources/api/netdog/src/wicked/dhcp.rs new file mode 100644 index 00000000000..3caba072868 --- /dev/null +++ b/sources/api/netdog/src/wicked/dhcp.rs @@ -0,0 +1,119 @@ +use crate::net_config::{Dhcp4ConfigV1, Dhcp4OptionsV1, Dhcp6ConfigV1, Dhcp6OptionsV1}; +use serde::Serialize; + +#[derive(Debug, Clone, Serialize, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub(crate) struct WickedDhcp4 { + #[serde(rename = "$unflatten=enabled")] + enabled: bool, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "$unflatten=route-priority")] + route_priority: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "$unflatten=defer-timeout")] + defer_timeout: Option, + #[serde(skip_serializing_if = "Option::is_none")] + flags: Option, +} + +impl Default for WickedDhcp4 { + fn default() -> Self { + WickedDhcp4 { + enabled: true, + route_priority: None, + defer_timeout: None, + flags: None, + } + } +} + +#[derive(Debug, Clone, Serialize, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub(crate) struct WickedDhcp6 { + #[serde(rename = "$unflatten=enabled")] + enabled: bool, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "$unflatten=defer-timeout")] + defer_timeout: Option, + #[serde(skip_serializing_if = "Option::is_none")] + flags: Option, +} + +impl Default for WickedDhcp6 { + fn default() -> Self { + WickedDhcp6 { + enabled: true, + defer_timeout: None, + flags: None, + } + } +} + +// 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 { + #[serde(rename = "$unflatten=optional")] + optional: (), +} + +impl From for WickedDhcp4 { + fn from(dhcp4: Dhcp4ConfigV1) -> Self { + match dhcp4 { + Dhcp4ConfigV1::DhcpEnabled(b) => WickedDhcp4 { + enabled: b, + ..Default::default() + }, + Dhcp4ConfigV1::WithOptions(o) => WickedDhcp4::from(o), + } + } +} + +impl From for WickedDhcp4 { + fn from(options: Dhcp4OptionsV1) -> 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, + } + } +} + +impl From for WickedDhcp6 { + fn from(dhcp6: Dhcp6ConfigV1) -> Self { + match dhcp6 { + Dhcp6ConfigV1::DhcpEnabled(b) => WickedDhcp6 { + enabled: b, + ..Default::default() + }, + Dhcp6ConfigV1::WithOptions(o) => WickedDhcp6::from(o), + } + } +} + +impl From for WickedDhcp6 { + fn from(options: Dhcp6OptionsV1) -> 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, + } + } +} diff --git a/sources/api/netdog/src/wicked.rs b/sources/api/netdog/src/wicked/mod.rs similarity index 56% rename from sources/api/netdog/src/wicked.rs rename to sources/api/netdog/src/wicked/mod.rs index 3762c0a6e31..ad2f13a8986 100644 --- a/sources/api/netdog/src/wicked.rs +++ b/sources/api/netdog/src/wicked/mod.rs @@ -3,10 +3,14 @@ //! //! 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. +mod dhcp; +mod static_address; + use crate::interface_name::InterfaceName; -use crate::net_config::{Dhcp4ConfigV1, Dhcp4OptionsV1, Dhcp6ConfigV1, Dhcp6OptionsV1}; +pub(crate) use dhcp::{WickedDhcp4, WickedDhcp6}; use serde::Serialize; use snafu::ResultExt; +pub(crate) use static_address::{WickedRoutes, WickedStaticAddress}; use std::fs; use std::path::Path; @@ -25,6 +29,12 @@ pub(crate) struct WickedInterface { #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "ipv6:dhcp")] pub(crate) ipv6_dhcp: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "ipv4:static")] + pub(crate) ipv4_static: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "ipv6:static")] + pub(crate) ipv6_static: Option, } #[derive(Debug, Serialize, PartialEq)] @@ -56,124 +66,18 @@ struct LinkDetection { require_link: (), } -#[derive(Debug, Clone, Serialize, PartialEq)] -#[serde(rename_all = "kebab-case")] -pub(crate) struct WickedDhcp4 { - #[serde(rename = "$unflatten=enabled")] - enabled: bool, - #[serde(skip_serializing_if = "Option::is_none")] - #[serde(rename = "$unflatten=route-priority")] - route_priority: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[serde(rename = "$unflatten=defer-timeout")] - defer_timeout: Option, - #[serde(skip_serializing_if = "Option::is_none")] - flags: Option, -} - -impl Default for WickedDhcp4 { - fn default() -> Self { - WickedDhcp4 { - enabled: true, - route_priority: None, - defer_timeout: None, - flags: None, - } - } -} - -#[derive(Debug, Clone, Serialize, PartialEq)] -#[serde(rename_all = "kebab-case")] -pub(crate) struct WickedDhcp6 { - #[serde(rename = "$unflatten=enabled")] - enabled: bool, - #[serde(skip_serializing_if = "Option::is_none")] - #[serde(rename = "$unflatten=defer-timeout")] - defer_timeout: Option, - #[serde(skip_serializing_if = "Option::is_none")] - flags: Option, -} - -impl Default for WickedDhcp6 { - fn default() -> Self { - WickedDhcp6 { - enabled: true, - defer_timeout: None, - flags: None, - } - } -} - -// 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 { - #[serde(rename = "$unflatten=optional")] - optional: (), -} - -impl From for WickedDhcp4 { - fn from(dhcp4: Dhcp4ConfigV1) -> Self { - match dhcp4 { - Dhcp4ConfigV1::DhcpEnabled(b) => WickedDhcp4 { - enabled: b, - ..Default::default() - }, - Dhcp4ConfigV1::WithOptions(o) => WickedDhcp4::from(o), - } - } -} - -impl From for WickedDhcp4 { - fn from(options: Dhcp4OptionsV1) -> 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, - } - } -} - -impl From for WickedDhcp6 { - fn from(dhcp6: Dhcp6ConfigV1) -> Self { - match dhcp6 { - Dhcp6ConfigV1::DhcpEnabled(b) => WickedDhcp6 { - enabled: b, - ..Default::default() - }, - Dhcp6ConfigV1::WithOptions(o) => WickedDhcp6::from(o), - } - } -} - -impl From for WickedDhcp6 { - fn from(options: Dhcp6OptionsV1) -> 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, +impl WickedInterface { + pub(crate) fn new(name: InterfaceName) -> Self { + Self { + name, + control: WickedControl::default(), + ipv4_dhcp: None, + ipv6_dhcp: None, + ipv4_static: None, + ipv6_static: None, } } -} -impl WickedInterface { /// Serialize the interface's configuration file pub(crate) fn write_config_file(&self) -> Result<()> { let mut cfg_path = Path::new(WICKED_CONFIG_DIR).join(self.name.to_string()); @@ -212,9 +116,13 @@ type Result = std::result::Result; mod tests { use super::*; use crate::net_config::{self, Interfaces, NetConfigV1}; + use handlebars::Handlebars; + use serde::Serialize; use std::path::PathBuf; use std::str::FromStr; + static NET_CONFIG_VERSIONS: &[u8] = &[1, 2]; + fn test_data() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_data") } @@ -223,10 +131,6 @@ mod tests { test_data().join("wicked") } - fn net_config() -> PathBuf { - test_data().join("net_config") - } - // Test the end-to-end trip: "net config from cmdline -> wicked -> serialized XML" #[test] fn interface_config_from_str() { @@ -263,17 +167,52 @@ mod tests { // Test the end to end trip: "net config -> wicked -> serialized XML" #[test] fn net_config_to_interface_config() { - let net_config_path = net_config().join("net_config.toml"); - let net_config = net_config::from_path(&net_config_path).unwrap().unwrap(); + let net_config_path = wicked_config().join("net_config.toml"); - let wicked_interfaces = net_config.as_wicked_interfaces(); - for interface in wicked_interfaces { - let mut path = wicked_config().join(interface.name.to_string()); - path.set_extension("xml"); - let expected = fs::read_to_string(path).unwrap(); - let generated = quick_xml::se::to_string(&interface).unwrap(); + for version in NET_CONFIG_VERSIONS { + let temp_config = tempfile::NamedTempFile::new().unwrap(); - assert_eq!(expected.trim(), generated) + render_config_template(&net_config_path, &temp_config, &version); + let net_config = net_config::from_path(&temp_config).unwrap().unwrap(); + let wicked_interfaces = net_config.as_wicked_interfaces(); + for interface in wicked_interfaces { + let mut path = wicked_config().join(interface.name.to_string()); + path.set_extension("xml"); + let expected = fs::read_to_string(path).unwrap(); + let generated = quick_xml::se::to_string(&interface).unwrap(); + dbg!(&generated); + + assert_eq!( + expected.trim(), + generated, + "failed test for net config version: '{}', interface: '{}'", + version, + interface.name.to_string() + ) + } + } + } + + fn render_config_template(template_path: P1, output_path: P2, version: &u8) + where + P1: AsRef, + P2: AsRef, + { + #[derive(Serialize)] + struct Context { + version: u8, } + + let output_path = output_path.as_ref(); + let template_path = template_path.as_ref(); + let template_str = fs::read_to_string(template_path).unwrap(); + + let mut hb = Handlebars::new(); + hb.register_template_string("template", &template_str) + .unwrap(); + + let context = Context { version: *version }; + let rendered = hb.render("template", &context).unwrap(); + fs::write(output_path, rendered).unwrap() } } diff --git a/sources/api/netdog/src/wicked/static_address.rs b/sources/api/netdog/src/wicked/static_address.rs new file mode 100644 index 00000000000..fa252fec195 --- /dev/null +++ b/sources/api/netdog/src/wicked/static_address.rs @@ -0,0 +1,146 @@ +use crate::net_config::{RouteTo, RouteV1, StaticConfigV1}; +use ipnet::IpNet; +use lazy_static::lazy_static; +use serde::Serialize; +use std::net::IpAddr; + +lazy_static! { + static ref DEFAULT_ROUTE_IPV4: IpNet = "0.0.0.0/0".parse().unwrap(); + static ref DEFAULT_ROUTE_IPV6: IpNet = "::/0".parse().unwrap(); +} + +#[derive(Default, Debug, Serialize, PartialEq)] +pub(crate) struct WickedStaticAddress { + #[serde(skip_serializing_if = "Option::is_none")] + address: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "route")] + routes: Option>, +} + +impl WickedStaticAddress { + /// Given the existence, or lack thereof, of addresses and routes, return a + /// WickedStaticAddress. The reason we return an `Option` here is that we don't want to + /// serialize an empty tag if no addresses or routes exist. + /// + /// If routes exist, but no static addresses exist, we drop them on the floor since there is a + /// guard for this condition when validating the network configuration, + pub(crate) fn maybe_new( + addresses: Option, + routes: Option>, + ) -> Option { + let static_addresses: Option> = addresses.map(StaticConfigV1::into); + // Wicked doesn't allow routes with DHCP, and routes are worthless without addresses, so + // don't bother creating anything without addresses + static_addresses.as_ref()?; + + Some(WickedStaticAddress { + address: static_addresses, + routes, + }) + } +} + +#[derive(Debug, Serialize, PartialEq)] +pub(crate) struct StaticAddress { + #[serde(rename = "$unflatten=local")] + local: IpNet, +} + +impl From for Vec { + fn from(s: StaticConfigV1) -> Self { + s.addresses + .into_iter() + .map(|a| StaticAddress { local: a }) + .collect() + } +} + +#[derive(Clone, Debug, Serialize, PartialEq)] +pub(crate) struct WickedRoute { + #[serde(rename = "$unflatten=destination")] + destination: IpNet, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "$unflatten=pref-source")] + pref_source: Option, + #[serde(skip_serializing_if = "Option::is_none")] + nexthop: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "$unflatten=priority")] + priority: Option, +} + +impl WickedRoute { + pub(crate) fn is_ipv4(&self) -> bool { + match self.destination { + IpNet::V4(_) => true, + IpNet::V6(_) => false, + } + } + + pub(crate) fn is_ipv6(&self) -> bool { + match self.destination { + IpNet::V4(_) => false, + IpNet::V6(_) => true, + } + } +} + +#[derive(Clone, Debug, Serialize, PartialEq)] +pub(crate) struct WickedNextHop { + #[serde(rename = "$unflatten=gateway")] + gateway: Option, +} + +impl From for WickedRoute { + fn from(route: RouteV1) -> Self { + let destination = match route.to { + RouteTo::DefaultRoute => match route.via.or(route.from) { + Some(IpAddr::V4(_)) => *DEFAULT_ROUTE_IPV4, + Some(IpAddr::V6(_)) => *DEFAULT_ROUTE_IPV6, + // If no gateway or from is given, assume the ipv4 default + None => *DEFAULT_ROUTE_IPV4, + }, + RouteTo::Ip(ip) => ip, + }; + + let nexthop = WickedNextHop { gateway: route.via }; + + WickedRoute { + destination, + nexthop: Some(nexthop), + pref_source: route.from, + priority: route.route_metric, + } + } +} + +// This type is not meant to be serialized, it's only purpose is to aggregate and categorize the +// ipv4/6 routes on their way to (maybe) being included in a `WickedRoute` which ends up being +// serialized to file. +#[derive(Clone, Default, Debug, PartialEq)] +pub(crate) struct WickedRoutes { + pub(crate) ipv4: Option>, + pub(crate) ipv6: Option>, +} + +impl WickedRoutes { + pub(crate) fn add_route(&mut self, route: WickedRoute) { + if route.is_ipv4() { + self.ipv4.get_or_insert_with(Vec::new).push(route) + } else if route.is_ipv6() { + self.ipv6.get_or_insert_with(Vec::new).push(route) + } + } +} + +impl From> for WickedRoutes { + fn from(routes: Vec) -> Self { + let mut wicked_routes = Self::default(); + for route in routes { + let wicked_route = WickedRoute::from(route); + wicked_routes.add_route(wicked_route); + } + wicked_routes + } +} diff --git a/sources/api/netdog/test_data/net_config/static_address/dhcp_and_routes.toml b/sources/api/netdog/test_data/net_config/static_address/dhcp_and_routes.toml new file mode 100644 index 00000000000..bcdc644b4af --- /dev/null +++ b/sources/api/netdog/test_data/net_config/static_address/dhcp_and_routes.toml @@ -0,0 +1,11 @@ +version = {{version}} + +[eno1] +dhcp4 = true + +[eno1.static4] +addresses = ["1.2.3.4/24", "2.3.4.5/25"] + +[[eno1.route]] +to = "4.5.6.7/24" +via = "10.0.0.1" diff --git a/sources/api/netdog/test_data/net_config/static_address/dhcp_and_static.toml b/sources/api/netdog/test_data/net_config/static_address/dhcp_and_static.toml new file mode 100644 index 00000000000..4e3190de3ea --- /dev/null +++ b/sources/api/netdog/test_data/net_config/static_address/dhcp_and_static.toml @@ -0,0 +1,7 @@ +version = {{version}} + +[eno1] +dhcp4 = true + +[eno1.static4] +addresses = ["1.2.3.4/24", "2.3.4.5/25"] diff --git a/sources/api/netdog/test_data/net_config/static_address/invalid_static_config.toml b/sources/api/netdog/test_data/net_config/static_address/invalid_static_config.toml new file mode 100644 index 00000000000..03b1d693cc6 --- /dev/null +++ b/sources/api/netdog/test_data/net_config/static_address/invalid_static_config.toml @@ -0,0 +1,6 @@ +version = {{version}} + +[eno1.static4] +addresses = ["1.2.3.4/24", "2.3.4.5/25"] +# `route-metric` is not a valid static4 option +route-metric = 100 diff --git a/sources/api/netdog/test_data/net_config/static_address/ipv4_in_static6.toml b/sources/api/netdog/test_data/net_config/static_address/ipv4_in_static6.toml new file mode 100644 index 00000000000..bf84ee093fd --- /dev/null +++ b/sources/api/netdog/test_data/net_config/static_address/ipv4_in_static6.toml @@ -0,0 +1,4 @@ +version = {{version}} + +[eno1.static6] +addresses = ["1.2.3.4/24", "2.3.4.5/25", "2001:dead:beef::2/64"] diff --git a/sources/api/netdog/test_data/net_config/static_address/ipv6_in_static4.toml b/sources/api/netdog/test_data/net_config/static_address/ipv6_in_static4.toml new file mode 100644 index 00000000000..03774c6740a --- /dev/null +++ b/sources/api/netdog/test_data/net_config/static_address/ipv6_in_static4.toml @@ -0,0 +1,4 @@ +version = {{version}} + +[eno1.static4] +addresses = ["2001:dead:beef::2/64", "10.0.0.10/24"] diff --git a/sources/api/netdog/test_data/net_config/static_address/net_config.toml b/sources/api/netdog/test_data/net_config/static_address/net_config.toml new file mode 100644 index 00000000000..bf9cec7c248 --- /dev/null +++ b/sources/api/netdog/test_data/net_config/static_address/net_config.toml @@ -0,0 +1,90 @@ +version = {{version}} + +# IPv4 static addresses/routes +[eno11.static4] +addresses = ["192.168.14.2/24"] + +[eno12.static4] +addresses = ["10.0.0.9/24"] + +[[eno12.route]] +to = "10.10.10.0/24" +via = "10.0.0.1" + +[eno13.static4] +addresses = ["192.168.14.2/24"] + +[[eno13.route]] +to = "9.9.0.0/16" +via = "192.168.1.1" + +[[eno13.route]] +to = "10.10.10.0/24" +via = "192.168.1.3" + +[eno14.static4] +addresses = ["10.0.0.10/24", "11.0.0.11/24"] + +[[eno14.route]] +to = "default" +via = "10.0.0.1" +route-metric = 100 + +[[eno14.route]] +to = "default" +via = "11.0.0.1" +route-metric = 200 + +# IPv6 static addresses/routes +[eno15.static6] +addresses = ["2001:cafe:face:beef::dead:dead/64"] + +[eno16.static6] +addresses = ["2001:dead:beef::2/64"] + +[[eno16.route]] +to = "default" +via = "2001:beef:beef::1" + +[eno17.static6] +addresses = ["3001:f00f:f00f::2/64", "3001:f00f:f00f::3/64"] + +[[eno17.route]] +to = "3001:dead:beef::2/64" +via = "3001:beef:beef::1" +route-metric = 100 + +[[eno17.route]] +to = "3001:dead:feed::2/64" +via = "3001:beef:beef::2" +route-metric = 200 + +# DHCP4/6 and static addresses +[eno18] +dhcp4 = true + +[eno18.static4] +addresses = ["10.0.0.10/24", "11.0.0.11/24"] + +[eno19] +dhcp6 = true + +[eno19.static6] +addresses = ["3001:f00f:f00f::2/64", "3001:f00f:f00f::3/64"] + +# Source IP +[eno20.static4] +addresses = ["192.168.14.5/24"] + +[[eno20.route]] +to = "10.10.10.0/24" +from = "192.168.14.5" +via = "192.168.14.25" + +[eno21.static6] +addresses = ["2001:dead:beef::2/64"] + +[[eno21.route]] +to = "3001:dead:beef::2/64" +from = "2001:dead:beef::2" +via = "2001:beef:beef::1" diff --git a/sources/api/netdog/test_data/net_config/static_address/no_dhcp_or_static.toml b/sources/api/netdog/test_data/net_config/static_address/no_dhcp_or_static.toml new file mode 100644 index 00000000000..2a1c5698f18 --- /dev/null +++ b/sources/api/netdog/test_data/net_config/static_address/no_dhcp_or_static.toml @@ -0,0 +1,3 @@ +version = {{version}} + +[eno1] diff --git a/sources/api/netdog/test_data/net_config/static_address/routes_no_addresses.toml b/sources/api/netdog/test_data/net_config/static_address/routes_no_addresses.toml new file mode 100644 index 00000000000..933d0287eec --- /dev/null +++ b/sources/api/netdog/test_data/net_config/static_address/routes_no_addresses.toml @@ -0,0 +1,5 @@ +version = {{version}} + +[[eno1.route]] +to = "4.5.6.7/24" +via = "10.0.0.1" diff --git a/sources/api/netdog/test_data/wicked/eno11.xml b/sources/api/netdog/test_data/wicked/eno11.xml new file mode 100644 index 00000000000..5dbd71fb438 --- /dev/null +++ b/sources/api/netdog/test_data/wicked/eno11.xml @@ -0,0 +1 @@ +eno11boot
192.168.14.2/24
diff --git a/sources/api/netdog/test_data/wicked/eno12.xml b/sources/api/netdog/test_data/wicked/eno12.xml new file mode 100644 index 00000000000..d8a371db5f1 --- /dev/null +++ b/sources/api/netdog/test_data/wicked/eno12.xml @@ -0,0 +1 @@ +eno12boot
10.0.0.9/24
10.10.10.0/2410.0.0.1
diff --git a/sources/api/netdog/test_data/wicked/eno13.xml b/sources/api/netdog/test_data/wicked/eno13.xml new file mode 100644 index 00000000000..7a1ddd018ad --- /dev/null +++ b/sources/api/netdog/test_data/wicked/eno13.xml @@ -0,0 +1 @@ +eno13boot
192.168.14.2/24
9.9.0.0/16192.168.1.110.10.10.0/24192.168.1.3
diff --git a/sources/api/netdog/test_data/wicked/eno14.xml b/sources/api/netdog/test_data/wicked/eno14.xml new file mode 100644 index 00000000000..28b55a50495 --- /dev/null +++ b/sources/api/netdog/test_data/wicked/eno14.xml @@ -0,0 +1 @@ +eno14boot
10.0.0.10/24
11.0.0.11/24
0.0.0.0/010.0.0.11000.0.0.0/011.0.0.1200
diff --git a/sources/api/netdog/test_data/wicked/eno15.xml b/sources/api/netdog/test_data/wicked/eno15.xml new file mode 100644 index 00000000000..c17f0fce1df --- /dev/null +++ b/sources/api/netdog/test_data/wicked/eno15.xml @@ -0,0 +1 @@ +eno15boot
2001:cafe:face:beef::dead:dead/64
diff --git a/sources/api/netdog/test_data/wicked/eno16.xml b/sources/api/netdog/test_data/wicked/eno16.xml new file mode 100644 index 00000000000..76f650b2d26 --- /dev/null +++ b/sources/api/netdog/test_data/wicked/eno16.xml @@ -0,0 +1 @@ +eno16boot
2001:dead:beef::2/64
::/02001:beef:beef::1
diff --git a/sources/api/netdog/test_data/wicked/eno17.xml b/sources/api/netdog/test_data/wicked/eno17.xml new file mode 100644 index 00000000000..510b690fca5 --- /dev/null +++ b/sources/api/netdog/test_data/wicked/eno17.xml @@ -0,0 +1 @@ +eno17boot
3001:f00f:f00f::2/64
3001:f00f:f00f::3/64
3001:dead:beef::2/643001:beef:beef::11003001:dead:feed::2/643001:beef:beef::2200
diff --git a/sources/api/netdog/test_data/wicked/eno18.xml b/sources/api/netdog/test_data/wicked/eno18.xml new file mode 100644 index 00000000000..8dc6e886763 --- /dev/null +++ b/sources/api/netdog/test_data/wicked/eno18.xml @@ -0,0 +1 @@ +eno18boottrue
10.0.0.10/24
11.0.0.11/24
diff --git a/sources/api/netdog/test_data/wicked/eno19.xml b/sources/api/netdog/test_data/wicked/eno19.xml new file mode 100644 index 00000000000..5816d3e7ab1 --- /dev/null +++ b/sources/api/netdog/test_data/wicked/eno19.xml @@ -0,0 +1 @@ +eno19boottrue
3001:f00f:f00f::2/64
3001:f00f:f00f::3/64
diff --git a/sources/api/netdog/test_data/wicked/eno20.xml b/sources/api/netdog/test_data/wicked/eno20.xml new file mode 100644 index 00000000000..4ea7420e425 --- /dev/null +++ b/sources/api/netdog/test_data/wicked/eno20.xml @@ -0,0 +1 @@ +eno20boot
192.168.14.5/24
10.10.10.0/24192.168.14.5192.168.14.25
diff --git a/sources/api/netdog/test_data/wicked/eno21.xml b/sources/api/netdog/test_data/wicked/eno21.xml new file mode 100644 index 00000000000..f40d0150d77 --- /dev/null +++ b/sources/api/netdog/test_data/wicked/eno21.xml @@ -0,0 +1 @@ +eno21boot
2001:dead:beef::2/64
3001:dead:beef::2/642001:dead:beef::22001:beef:beef::1
diff --git a/sources/api/netdog/test_data/wicked/net_config.toml b/sources/api/netdog/test_data/wicked/net_config.toml new file mode 100644 index 00000000000..b3ad92b7452 --- /dev/null +++ b/sources/api/netdog/test_data/wicked/net_config.toml @@ -0,0 +1,141 @@ +version = {{version}} + +[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 + +[eno7] +dhcp4 = true + +[eno7.dhcp6] +enabled = true +optional = true + +[eno8.dhcp4] +enabled = true +optional = true + +[eno8.dhcp6] +enabled = true +optional = true + +[eno9.dhcp4] +enabled = true +optional = true + +[eno10.dhcp6] +enabled = true +optional = true + +{{#if (eq version 2)}} +# IPv4 static addresses/routes +[eno11.static4] +addresses = ["192.168.14.2/24"] + +[eno12.static4] +addresses = ["10.0.0.9/24"] + +[[eno12.route]] +to = "10.10.10.0/24" +via = "10.0.0.1" + +[eno13.static4] +addresses = ["192.168.14.2/24"] + +[[eno13.route]] +to = "9.9.0.0/16" +via = "192.168.1.1" + +[[eno13.route]] +to = "10.10.10.0/24" +via = "192.168.1.3" + +[eno14.static4] +addresses = ["10.0.0.10/24", "11.0.0.11/24"] + +[[eno14.route]] +to = "default" +via = "10.0.0.1" +route-metric = 100 + +[[eno14.route]] +to = "default" +via = "11.0.0.1" +route-metric = 200 + +# IPv6 static addresses/routes +[eno15.static6] +addresses = ["2001:cafe:face:beef::dead:dead/64"] + +[eno16.static6] +addresses = ["2001:dead:beef::2/64"] + +[[eno16.route]] +to = "default" +via = "2001:beef:beef::1" + +[eno17.static6] +addresses = ["3001:f00f:f00f::2/64", "3001:f00f:f00f::3/64"] + +[[eno17.route]] +to = "3001:dead:beef::2/64" +via = "3001:beef:beef::1" +route-metric = 100 + +[[eno17.route]] +to = "3001:dead:feed::2/64" +via = "3001:beef:beef::2" +route-metric = 200 + +# DHCP4/6 and static addresses +[eno18] +dhcp4 = true + +[eno18.static4] +addresses = ["10.0.0.10/24", "11.0.0.11/24"] + +[eno19] +dhcp6 = true + +[eno19.static6] +addresses = ["3001:f00f:f00f::2/64", "3001:f00f:f00f::3/64"] + +# Source IP +[eno20.static4] +addresses = ["192.168.14.5/24"] + +[[eno20.route]] +to = "10.10.10.0/24" +from = "192.168.14.5" +via = "192.168.14.25" + +[eno21.static6] +addresses = ["2001:dead:beef::2/64"] + +[[eno21.route]] +to = "3001:dead:beef::2/64" +from = "2001:dead:beef::2" +via = "2001:beef:beef::1" +{{/if}}