blob: d10e88c189f9d4d7a278e93cdd79727a4b5c963c [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use crate::error;
use crate::lifmgr;
use serde_derive::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashSet;
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use valico::json_schema::{self, schema};
/// Interface types defined by the OpenConfig interfaces model.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone)]
#[serde(deny_unknown_fields, rename_all = "SCREAMING_SNAKE_CASE")]
pub enum InterfaceType {
IfEthernet,
IfAggregate,
IfLoopback,
IfRoutedVlan,
IfSonet,
IfTunnelGre4,
IfTunnelGre6,
IfUplink,
}
/// The possible interface operational states from RFC2863 "Standard Interfaces MIB".
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone)]
#[serde(deny_unknown_fields, rename_all = "SCREAMING_SNAKE_CASE")]
pub enum OperState {
Up,
Down,
Unknown,
Testing,
NotPresent,
LowerLayerDown,
}
/// The possible interface admin states from RFC2863 "Standard Interfaces MIB".
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone)]
#[serde(deny_unknown_fields, rename_all = "SCREAMING_SNAKE_CASE")]
pub enum AdminState {
Up,
Down,
Testing,
}
/// When `auto-negotiate` is true, this optionally sets the duplex mode that will be advertised to
/// the peer. If unspecified, the interface should negotiate the duplex mode directly (typically
/// full-duplex). When auto-negotiate is false, this sets the duplex mode on the interface directly.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone)]
#[serde(deny_unknown_fields, rename_all = "SCREAMING_SNAKE_CASE")]
pub enum DuplexMode {
Full,
Half,
}
/// Defines VLAN interface types.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone)]
#[serde(deny_unknown_fields, rename_all = "SCREAMING_SNAKE_CASE")]
pub enum InterfaceMode {
Access,
Trunk,
}
/// When `auto-negotiate` is true, this optionally sets the port-speed mode that will be advertised
/// to the peer for negotiation. If unspecified, it is expected that the interface will select the
/// highest speed available based on negotiation. When auto-negotiate is set to false, sets the
/// link speed to a fixed value.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone)]
#[serde(deny_unknown_fields)]
pub enum PortSpeed {
#[serde(alias = "SPEED_10MB")]
Speed10mb,
#[serde(alias = "SPEED_100MB")]
Speed100mb,
#[serde(alias = "SPEED_1G")]
Speed1g,
#[serde(alias = "SPEED_2500MB")]
Speed2500mb,
#[serde(alias = "SPEED_5G")]
Speed5g,
#[serde(alias = "SPEED_10G")]
Speed10g,
#[serde(alias = "SPEED_UNKNOWN")]
SpeedUnknown,
}
// TODO(cgibson): VLANs.
// TODO(cgibson): ACLs.
// TODO(cgibson): WLAN.
// TODO(cgibson): Need to figure out versioning. Having "unknown" fields and "flatten"'ing them
// into an `extras` field might be an interesting experiment.
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct DeviceConfig {
pub device: Device,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct Device {
#[serde(skip_serializing_if = "Option::is_none")]
pub interfaces: Option<Vec<Interfaces>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub acls: Option<Acls>,
#[serde(skip_serializing_if = "Option::is_none")]
pub services: Option<Services>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct Interfaces {
pub interface: Interface,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct Interface {
// Certain types of interface (e.g. RoutedVlan) do not have a device_id.
#[serde(skip_serializing_if = "Option::is_none")]
pub device_id: Option<String>,
// Every Interface must have a config definition.
pub config: InterfaceConfig,
// If oper_state is omitted, then the default is `OperState::Up`.
#[serde(skip_serializing_if = "Option::is_none")]
pub oper_state: Option<OperState>,
// An interface must contain exactly one: 'subinterfaces', 'switched_vlan', or 'routed_vlan'.
#[serde(skip_serializing_if = "Option::is_none")]
pub subinterfaces: Option<Vec<Subinterface>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub switched_vlan: Option<SwitchedVlan>,
#[serde(skip_serializing_if = "Option::is_none")]
pub routed_vlan: Option<RoutedVlan>,
// ethernet can be omitted and defaults will be applied.
#[serde(skip_serializing_if = "Option::is_none")]
pub ethernet: Option<Ethernet>,
// tcp_offload can be omitted and the default will be applied.
#[serde(skip_serializing_if = "Option::is_none")]
pub tcp_offload: Option<bool>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct InterfaceConfig {
pub name: String,
#[serde(rename = "type")]
pub interface_type: InterfaceType,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct Subinterface {
// If admin_state is omitted, then the default is AdminState::Up.
#[serde(skip_serializing_if = "Option::is_none")]
pub admin_state: Option<AdminState>,
// A subinterface must have at least one IP address configuration.
#[serde(skip_serializing_if = "Option::is_none")]
pub ipv4: Option<IpAddressConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ipv6: Option<IpAddressConfig>,
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct IpAddressConfig {
pub addresses: Vec<IpAddress>,
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct IpAddress {
// If omitted, the default is to enable a DHCP client on this interface.
#[serde(skip_serializing_if = "Option::is_none")]
pub dhcp_client: Option<bool>,
// If an IP address is provided, it must be paired with a prefix length.
#[serde(skip_serializing_if = "Option::is_none")]
pub ip: Option<std::net::IpAddr>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prefix_length: Option<u8>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct SwitchedVlan {
pub interface_mode: InterfaceMode,
#[serde(skip_serializing_if = "Option::is_none")]
pub access_vlan: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trunk_vlans: Option<Vec<u16>>,
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct RoutedVlan {
pub vlan_id: u16,
#[serde(skip_serializing_if = "Option::is_none")]
pub ipv4: Option<IpAddressConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ipv6: Option<IpAddressConfig>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct Ethernet {
#[serde(skip_serializing_if = "Option::is_none")]
pub config: Option<EthernetConfig>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct EthernetConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_negotiate: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub duplex_mode: Option<DuplexMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_flow_control: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mac_address: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mtu: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub port_speed: Option<PortSpeed>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct Acls {}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct Services {
#[serde(skip_serializing_if = "Option::is_none")]
pub dhcp_server: Option<DhcpServer>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ip_forwarding: Option<IpForwarding>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct DhcpServer {
pub enabled: bool,
pub dhcp_pool: DhcpPool,
#[serde(skip_serializing_if = "Option::is_none")]
pub static_ip_allocations: Option<Vec<StaticIpAllocations>>,
pub interfaces: String,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct DhcpPool {
pub start: String,
pub end: String,
pub lease_time: String,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct StaticIpAllocations {
pub device_name: String,
pub mac_address: String,
pub ip_address: String,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct IpForwarding {
pub enabled: bool,
}
#[derive(Debug, PartialEq)]
struct DeviceConfigPaths {
user_config_path: PathBuf,
factory_config_path: PathBuf,
device_schema_path: PathBuf,
}
#[derive(Debug, PartialEq)]
pub struct Config {
device_config: Option<DeviceConfig>,
startup_path: Option<PathBuf>,
paths: DeviceConfigPaths,
}
const UNNAMED_BRIDGE: &str = "unnamed_bridge";
/// Converts a Valico JSON SchemaError to a `String`.
fn schema_error(error: schema::SchemaError) -> String {
match error {
schema::SchemaError::WrongId => String::from("Wrong Id"),
schema::SchemaError::IdConflicts => String::from("Id conflicts"),
schema::SchemaError::NotAnObject => String::from("Not an object"),
schema::SchemaError::UrlParseError(p) => String::from(format!("Url parse error: {}", p)),
schema::SchemaError::UnknownKey(key) => String::from(format!("Unknown key: {}", key)),
schema::SchemaError::Malformed { path, detail } => {
String::from(format!("Malformed: {}, {}", path, detail))
}
}
}
impl Config {
/// Creates a new Config object with the given user, factory, and device schema paths.
pub fn new<P: Into<PathBuf>>(user_path: P, factory_path: P, device_schema: P) -> Config {
Config {
device_config: None,
startup_path: None,
paths: DeviceConfigPaths {
user_config_path: user_path.into(),
factory_config_path: factory_path.into(),
device_schema_path: device_schema.into(),
},
}
}
/// Returns the user config path.
pub fn user_config_path(&self) -> &Path {
self.paths.user_config_path.as_path()
}
/// Returns the factory config path.
pub fn factory_config_path(&self) -> &Path {
self.paths.factory_config_path.as_path()
}
/// Returns the device schema path.
pub fn device_schema_path(&self) -> &Path {
self.paths.device_schema_path.as_path()
}
/// Returns the path of the config that was read at startup
///
/// If no configuration file has been read yet, then returns an empty path.
pub fn startup_path(&self) -> &Path {
if let Some(p) = &self.startup_path {
p.as_path()
} else {
Path::new("")
}
}
/// Loads the relevant configuration file.
///
/// This method tries to load the user configuration file. If the user configuration file does
/// not exist (e.g. first boot after OOBE, FDR, etc), then fallback to loading the factory
/// configuration file.
///
/// If this method returns successfully, there will be a newly loaded and properly validated
/// deserialized configuration available.
pub async fn load_config(&mut self) -> error::Result<()> {
let loaded_config;
let loaded_path;
match self.try_load_config(&self.user_config_path()).await {
Ok(c) => {
loaded_config = c;
loaded_path = self.paths.user_config_path.to_path_buf();
}
Err(e) => {
warn!("Failed to load user config: {}", e);
loaded_config = self.try_load_config(&self.factory_config_path()).await?;
loaded_path = self.paths.factory_config_path.to_path_buf();
}
}
match self.validate_with_schema(&loaded_config).await {
Ok(_) => {
self.device_config = Some(serde_json::from_value(loaded_config).map_err(|e| {
error::NetworkManager::CONFIG(error::Config::FailedToDeserializeConfig {
path: String::from(loaded_path.to_string_lossy()),
error: e.to_string(),
})
})?);
self.startup_path = Some(loaded_path);
return self.final_validation().await;
}
Err(e) => Err(e),
}
}
/// Attempts to load a given configuration file.
///
/// Returns the [`serde_json::Value`] of the loaded configuration file. It is important to note
/// that if the config fails further validation then it will not be used.
///
/// # Errors
///
/// If the config file does not exist, cannot be read, or is not a file, then an error result
/// of `ConfigNotLoaded` will be returned.
///
/// If the loaded configuration fails to deserialize, then an error result of
/// `FailedToDeserializeConfig` will be returned.
async fn try_load_config(&self, config_path: &Path) -> error::Result<Value> {
info!("Trying to load from: {}", String::from(config_path.to_string_lossy()));
if config_path.is_file() {
let mut contents = String::new();
let mut f = File::open(config_path).map_err(|e| {
error::NetworkManager::CONFIG(error::Config::ConfigNotLoaded {
path: String::from(config_path.to_string_lossy()),
error: e.to_string(),
})
})?;
f.read_to_string(&mut contents).map_err(|e| {
error::NetworkManager::CONFIG(error::Config::ConfigNotLoaded {
path: String::from(config_path.to_string_lossy()),
error: e.to_string(),
})
})?;
// Note that counter to intuition it is faster to read the full configuration file
// into memory and deserialize it using serde_json::from_str(), than using
// serde_json::from_reader(), see: https://github.com/serde-rs/json/issues/160.
let json: Value = serde_json::from_str(&contents).map_err(|e| {
error::NetworkManager::CONFIG(error::Config::FailedToDeserializeConfig {
path: String::from(config_path.to_string_lossy()),
error: e.to_string(),
})
})?;
return Ok(json);
}
Err(error::NetworkManager::CONFIG(error::Config::ConfigNotFound {
path: String::from(config_path.to_string_lossy()),
}))
}
/// Ensure that the configuration validates against the device schema.
///
/// # Errors
///
/// If the actual device schema fails to load, then Valico will emit an appropriate error
/// message that can be used to fix device schema errors.
///
/// If the config does not validate against the device schema, then an error result
/// `FailedToValidateConfig` will be returned providing information with what went wrong.
async fn validate_with_schema(&self, config: &Value) -> error::Result<()> {
info!("Validating config against the device schema");
let device_schema = self.try_load_config(&self.device_schema_path()).await?;
let mut scope = json_schema::Scope::new();
let schema = scope.compile_and_return(device_schema, false).map_err(|e| {
error!("Failed to validate schema: {:?}", e);
error::NetworkManager::CONFIG(error::Config::FailedToValidateConfig {
path: String::from(self.device_schema_path().to_string_lossy()),
error: schema_error(e),
})
})?;
// Use the JSON Schema to give the JSON config some formal structure, but we need to
// do most of the validation ourselves.
let result = schema.validate(config);
if !result.is_strictly_valid() {
let mut err_msgs = Vec::new();
for e in &result.errors {
err_msgs.push(format!("{} at {}", e.get_title(), e.get_path()).into_boxed_str());
}
for u in &result.missing {
err_msgs.push(format!("Device Config missing: {}", u).into_boxed_str());
}
// The ordering in which valico emits these errors is unstable. Sort error messages so
// that the resulting message is predictable.
err_msgs.sort_unstable();
return Err(error::NetworkManager::CONFIG(error::Config::FailedToValidateConfig {
path: String::from(self.startup_path().to_string_lossy()),
error: err_msgs.join(", "),
}));
}
Ok(())
}
/// Validates an [`config::Interface`]'s configuration.
///
/// An Interface definition is valid if it has exactly one configuration type (e.g.
/// subinterface, switched_vlan, or routed_vlan).
fn validate_interface_config(&self, intf: &Interface) -> error::Result<()> {
// TODO(cgibson): Try using serde's 'externally tagged' enum representation to remove
// this validation step: https://serde.rs/enum-representations.html
if intf.subinterfaces.is_some()
&& intf.switched_vlan.is_none()
&& intf.routed_vlan.is_none()
{
return Ok(());
} else if intf.switched_vlan.is_some()
&& intf.subinterfaces.is_none()
&& intf.routed_vlan.is_none()
{
return Ok(());
} else if intf.routed_vlan.is_some()
&& intf.subinterfaces.is_none()
&& intf.switched_vlan.is_none()
{
return Ok(());
}
Err(error::NetworkManager::CONFIG(error::Config::FailedToValidateConfig {
path: String::from(self.startup_path().to_string_lossy()),
error: concat!(
"Interface must be exactly one of either: ",
"'subinterfaces', 'routed_vlan', or 'switched_vlan'"
)
.to_string(),
}))
}
/// Validates a [`config::InterfaceType`].
///
/// If an Interface's type is [`InterfaceType::IfUplink`], then the Interface must have a
/// [`Interface::Subinterfaces`] definition.
fn validate_interface_types(&self, intf: &Interface) -> error::Result<()> {
match intf.config.interface_type {
InterfaceType::IfUplink => {
if intf.subinterfaces.is_none() {
return Err(error::NetworkManager::CONFIG(
error::Config::FailedToValidateConfig {
path: String::from(self.startup_path().to_string_lossy()),
error:
"Interface type is 'IF_UPLINK' but does not define a 'subinterface'"
.to_string(),
},
));
}
if let Some(subifs) = intf.subinterfaces.as_ref() {
self.validate_subinterfaces(subifs)?;
}
}
InterfaceType::IfRoutedVlan => {
if intf.routed_vlan.is_none() {
return Err(error::NetworkManager::CONFIG(
error::Config::FailedToValidateConfig {
path: String::from(self.startup_path().to_string_lossy()),
error: concat!(
"Interface type is 'IF_ROUTED_VLAN' but does ",
"not define a 'routed_vlan'"
)
.to_string(),
},
));
}
}
// Add additional type validation here.
_ => return Ok(()),
}
Ok(())
}
/// Validates an [`config::IpAddress`].
fn validate_ip_address(&self, addr: &IpAddress) -> error::Result<()> {
let has_static = addr.ip.is_some() || addr.prefix_length.is_some();
let valid_static = !(addr.ip.is_some() ^ addr.prefix_length.is_some());
let dhcp = addr.dhcp_client.unwrap_or(false);
let valid_xor = dhcp ^ has_static;
if valid_xor && valid_static {
Ok(())
} else {
Err(error::NetworkManager::CONFIG(error::Config::Malformed {
msg: format!("Invalid IpAddress configuration: {:?}", addr),
}))
}
}
/// Validates each [`config::Subinterface`].
fn validate_subinterfaces(&self, subinterfaces: &Vec<Subinterface>) -> error::Result<()> {
for subif in subinterfaces.into_iter() {
if let Some(v4addr) = &subif.ipv4 {
for a in v4addr.addresses.iter() {
self.validate_ip_address(&a)?;
}
}
if let Some(v6addr) = &subif.ipv6 {
for a in v6addr.addresses.iter() {
self.validate_ip_address(&a)?;
}
}
}
Ok(())
}
/// Validates an [`config::Interface`] configuration.
fn validate_interface(&self, intf: &Interface) -> error::Result<()> {
self.validate_interface_config(&intf)?;
self.validate_interface_types(&intf)?;
Ok(())
}
/// Performs an additional layer of validation checks that cannot be expressed in JSON Schema.
async fn final_validation(&self) -> error::Result<()> {
let mut intf_names = HashSet::new();
let intfs = match self.interfaces() {
Some(intfs) => intfs,
None => {
return Err(error::NetworkManager::CONFIG(error::Config::NotFound {
msg: format!("Config contains no interfaces"),
}));
}
};
for intfs in intfs.iter() {
self.validate_interface(&intfs.interface)?;
// Interface names must be unique.
if intf_names.contains(&intfs.interface.config.name) {
return Err(error::NetworkManager::CONFIG(error::Config::FailedToValidateConfig {
path: String::from(self.startup_path().to_string_lossy()),
error: format!(
"Duplicate interface names detected: '{}'",
intfs.interface.config.name
),
}));
}
intf_names.insert(intfs.interface.config.name.clone());
}
Ok(())
}
/// Returns [`config::Device`] from the config.
pub fn device(&self) -> error::Result<&Device> {
self.device_config.as_ref().map(|c| &c.device).ok_or_else(|| {
error::NetworkManager::CONFIG(error::Config::NotFound {
msg: format!("Device was not found yet. Is the config loaded?"),
})
})
}
/// Returns all the configured [`config::Interfaces`].
pub fn interfaces(&self) -> Option<&Vec<Interfaces>> {
self.device().ok().and_then(|x| x.interfaces.as_ref())
}
/// Returns the [`config::Interface`] that matches the device_id contained in `topo_path`.
pub fn get_interface_by_device_id(&self, topo_path: &str) -> Option<&Interface> {
if let Some(ifs) = self.interfaces() {
for intfs in ifs.iter() {
if let Some(d) = &intfs.interface.device_id {
if topo_path.contains(d.as_str()) {
return Some(&intfs.interface);
}
}
}
}
None
}
/// Returns `true` if the device id from the `topo_path` is an uplink.
///
/// An "uplink" is defined by having an [`InterfaceType::IfUplink`] and having a "subinterface"
/// definition.
pub fn device_id_is_a_wan_uplink(&self, topo_path: &str) -> bool {
self.get_interface_by_device_id(topo_path)
.map(|intf| match intf.config.interface_type {
InterfaceType::IfUplink => return intf.subinterfaces.is_some(),
_ => return false,
})
.unwrap_or(false)
}
/// Returns the name of the WAN interface.
pub fn get_wan_interface_name(&self, topo_path: &str) -> error::Result<String> {
if self.device_id_is_a_wan_uplink(topo_path) {
if let Some(intf) = self.get_interface_by_device_id(topo_path) {
return Ok(intf.config.name.clone());
}
}
Err(error::NetworkManager::CONFIG(error::Config::NotFound {
msg: format!("Getting WAN interface name for {} failed.", topo_path),
}))
}
/// Returns a tuple of IPv4 and IPv6 [`config::IpAddress`]'s for this interface.
///
/// The 'switched_vlan' configuration does not support IP addressing, so any `Interface` that
/// has a 'switched_vlan' configuration will return `None`.
pub fn get_ip_address<'a>(
&self,
intf: &'a Interface,
) -> error::Result<(Option<&'a IpAddress>, Option<&'a IpAddress>)> {
if let Some(subifs) = &intf.subinterfaces {
if subifs.is_empty() {
return Err(error::NetworkManager::CONFIG(error::Config::Malformed {
msg: format!("Interface must have at least one 'subinterface' definition"),
}));
}
// TODO(cgibson): LIFProperties doesn't support vectors of IP addresses yet. fxb/42315.
if subifs.len() != 1 {
warn!("LIFProperties does not support multiple addresses yet.")
}
let subif = &subifs[0];
let v4addr = subif.ipv4.as_ref().and_then(|c| c.addresses.iter().next());
let v6addr = subif.ipv6.as_ref().and_then(|c| c.addresses.iter().next());
return Ok((v4addr, v6addr));
}
Err(error::NetworkManager::CONFIG(error::Config::NotFound {
msg: format!(
"Could not find an IP config defined on the provided interface: {:?}",
intf
),
}))
}
/// Updates [`lifmgr::LIFProperties`] with the given IP address configuration.
///
/// In an IPv4-only network, with DHCP client disabled, and no static IP address configuration,
/// then there will be no address configured for this interface.
///
/// In an IPv6-only network, we currently have no plans to support DHCPv6 and there is no
/// static IPv6 address configuration, we will rely on SLAAC to get a V6 address.
///
/// Blended V4/V6 networks with IPv4 DHCP client disabled and no static IP address will rely on
/// SLAAC to get an IPv6 address.
///
/// If `ipconfig` is `None` then it is the same as an IPv6-only network, we rely on SLAAC to get
/// an address.
fn set_ip_address_config(
&self,
properties: &mut lifmgr::LIFProperties,
ipconfig: Option<&IpAddress>,
) {
if let Some(c) = ipconfig {
if let Some(dhcp_client) = c.dhcp_client {
properties.dhcp = dhcp_client;
}
// TODO(42315): LIFProperties doesn't support IPv6 addresses yet.
match (c.ip, c.prefix_length) {
(Some(address), Some(prefix)) => {
properties.address = Some(lifmgr::LifIpAddr { address, prefix });
}
_ => {}
}
}
// TODO(42315): LIF manager throws an error if both a DHCP client and a static IP address
// configuration are set. We don't want to be generating invalid LIFProperties, so favor
// the static IP configuration and turn off DHCP.
if properties.dhcp && properties.address.is_some() {
warn!("DHCP client and static IP cannot be configured at once: Disabling DHCP.");
properties.dhcp = false;
}
}
/// Returns a WAN-specific [`lifmgr::LIFProperties`] based on the running configuration.
///
/// Configures the WAN uplink's LIFProperties, initially discovers the interface that contains
/// matches the device ID from the topological path. Then sets the LIFProperties for the admin
/// state of the interface as well as configures the IP address.
pub fn create_wan_properties(&self, topo_path: &str) -> error::Result<lifmgr::LIFProperties> {
let mut properties =
crate::lifmgr::LIFProperties { enabled: true, dhcp: false, address: None };
let intf = match self.get_interface_by_device_id(topo_path) {
Some(x) => x,
None => {
return Err(error::NetworkManager::CONFIG(error::Config::Malformed {
msg: format!(
"Cannot find an Interface matching `device_id` from topo path: {}",
topo_path
),
}));
}
};
let subifs = match &intf.subinterfaces {
Some(subif) => subif,
None => {
return Err(error::NetworkManager::CONFIG(error::Config::Malformed {
msg: format!("An uplink must have a 'subinterface' configuration"),
}));
}
};
// TODO(42315): LIFProperties does not support multiple addresses yet.
if subifs.len() != 1 {
return Err(error::NetworkManager::CONFIG(error::Config::NotSupported {
msg: format!("Multiple subinterfaces on a single interface are not supported"),
}));
}
if let Some(subif) = subifs.get(0) {
match subif.admin_state {
Some(AdminState::Up) => {
properties.enabled = true;
}
Some(AdminState::Down) => {
warn!("WAN subinterface is admin down by config");
properties.enabled = false;
}
Some(AdminState::Testing) => {
warn!("Admin state 'TESTING' is not supported; defaulting to 'Up'");
properties.enabled = true;
}
_ => {
// Subinterface's are enabled by default.
properties.enabled = true;
}
}
}
let (v4addr, v6addr) = self.get_ip_address(intf)?;
// TODO(42316): LIFProperties doesn't support IPv6 addresses yet.
if v6addr.is_some() {
warn!("Setting IPv6 addresses is not supported yet");
}
self.set_ip_address_config(&mut properties, v4addr);
Ok(properties)
}
/// Returns a vector of [`config::Interface`]'s that contain a [`config::RoutedVlan`].
///
/// If no `Interface`'s are defined as being a `RoutedVlan`, then this method will return an
/// empty Iterator.
///
/// If there are no interfaces configured, then this method returns `None`.
pub fn get_routed_vlan_interfaces(&self) -> Option<impl Iterator<Item = &Interface>> {
self.interfaces().map(|ifs| {
ifs.iter().filter_map(|intfs| {
if intfs.interface.routed_vlan.is_some() {
Some(&intfs.interface)
} else {
None
}
})
})
}
/// Returns the [`SwitchedVlan`] that matches the device id contained in `topo_path`.
pub fn get_switched_vlan_by_device_id(&self, topo_path: &str) -> error::Result<&SwitchedVlan> {
self.get_interface_by_device_id(topo_path)
.and_then(|intf| intf.switched_vlan.as_ref())
.ok_or_else(|| {
error::NetworkManager::CONFIG(error::Config::NotFound {
msg: format!("Failed to find interface matching: {}", topo_path),
})
})
}
/// Returns `true` if `topo_path` resolves to a [`config::SwitchedVlan`].
pub fn device_id_is_a_switched_vlan(&self, topo_path: &str) -> bool {
self.get_switched_vlan_by_device_id(topo_path).is_ok()
}
/// Resolves a [`config::SwitchedVlan`] to it's [`config::RoutedVlan`] configuration.
///
/// # Errors
///
/// If the switched VLAN does not resolve to a routed VLAN, then a `NotFound` error will be
/// returned.
///
/// If the switched VLAN port does not have the correct configuration based on its interface
/// mode, then a `Malformed` error will be returned.
pub fn resolve_to_routed_vlans(
&self,
switched_vlan: &SwitchedVlan,
) -> error::Result<&RoutedVlan> {
let mut switched_vlan_ids = Vec::new();
match switched_vlan.interface_mode {
InterfaceMode::Access => match switched_vlan.access_vlan {
Some(vid) => switched_vlan_ids.push(vid),
None => {
return Err(error::NetworkManager::CONFIG(error::Config::Malformed {
msg: format!(
"Expecting access port to have 'access_vlan': {:?}",
switched_vlan
),
}));
}
},
// TODO(cgibson): Implement trunk VLANs when netstack supports it.
InterfaceMode::Trunk => {
return Err(error::NetworkManager::CONFIG(error::Config::NotSupported {
msg: format!("Trunk VLANs are not supported yet."),
}));
}
}
if let Some(it) = self.get_routed_vlan_interfaces() {
for intf in it {
// Safe to unwrap() here because `get_routed_vlan_interfaces()` checked for us
// already.
let routed_vlan = intf.routed_vlan.as_ref().unwrap();
if switched_vlan_ids.contains(&routed_vlan.vlan_id) {
return Ok(&routed_vlan);
}
}
}
Err(error::NetworkManager::CONFIG(error::Config::NotFound {
msg: format!(
"Switched VLAN port does not resolve to a routed VLAN: {:?}",
switched_vlan
),
}))
}
/// Checks that all `ports` resolve to a single [`config::RoutedVlan`].
pub fn all_ports_have_same_bridge<'a>(
&'a self,
ports: impl Iterator<Item = error::Result<&'a SwitchedVlan>>,
) -> error::Result<&'a RoutedVlan> {
let mut ports = ports.peekable();
if ports.peek().is_none() {
warn!("Provided list of ports was empty?");
return Err(error::NetworkManager::CONFIG(error::Config::Malformed {
msg: format!("Provided list of ports was empty?"),
}));
}
let mut routed_vlan = None;
for p in ports {
let r = self.resolve_to_routed_vlans(p?)?;
routed_vlan = match routed_vlan {
None => Some(r),
Some(v) => {
if v == r {
Some(r)
} else {
return Err(error::NetworkManager::CONFIG(error::Config::Malformed {
msg: format!(
"switched_vlan ports do not resolve to the same RoutedVlan"
),
}));
}
}
}
}
routed_vlan.ok_or_else(|| {
error::NetworkManager::CONFIG(error::Config::Malformed {
msg: format!("switched_vlan ports do not resolve to the same RoutedVlan"),
})
})
}
/// Returns the name of the bridge.
pub fn get_bridge_name(&self, target: &RoutedVlan) -> &str {
self.get_routed_vlan_interfaces()
.and_then(|mut vlan_ifs| {
vlan_ifs.find_map(|intf| {
intf.routed_vlan.as_ref().and_then(|vlan| {
if vlan.vlan_id == target.vlan_id {
Some(intf.config.name.as_str())
} else {
None
}
})
})
})
.unwrap_or(UNNAMED_BRIDGE)
}
/// Creates a new [`lifmgr::LIFProperties`] for the provided `RoutedVlan`.
pub fn create_routed_vlan_properties(
&self,
bridge: &RoutedVlan,
) -> error::Result<lifmgr::LIFProperties> {
let mut properties =
crate::lifmgr::LIFProperties { enabled: true, dhcp: false, address: None };
// TODO(42316): LIFProperties doesn't support IPv6 addresses yet.
if bridge.ipv6.is_some() {
warn!("Setting IPv6 addresses is not supported yet.");
}
bridge.ipv4.as_ref().and_then(|c| {
if c.addresses.len() > 1 {
warn!("Configuring multiple IPv4 addresses is not supported yet");
}
Some(())
});
let v4addr = bridge.ipv4.as_ref().and_then(|c| c.addresses.iter().next());
self.set_ip_address_config(&mut properties, v4addr);
// TODO(42316): Until LIFProperties supports IPv6 addresses, we have the possibility of
// generating properties that do not have an IP configuration. Make sure that we have
// at least DHCP client enabled.
if !properties.dhcp && properties.address.is_none() {
properties.dhcp = true;
}
Ok(properties)
}
/// Returns configured VLAN IDs for the given device that matches `topo_path`.
///
/// If the interface is a `SwitchedVlan` then the VLAN IDs will be depend on if the interface
/// mode is either a `trunk` or `access` port.
///
/// If the interface is a `RoutedVlan` then the VLAN ID will be a vector of a single element
/// containing the `routed_vlan`'s VLAN ID.
///
/// If the interface is a `subinterface` then the vector will be empty.
pub fn get_vlans(&self, topo_path: &str) -> Vec<u16> {
let mut vids = Vec::new();
if let Some(intf) = self.get_interface_by_device_id(topo_path) {
match &intf.routed_vlan {
Some(r) => {
// routed_vlan's have a single VLAN ID only.
return vec![r.vlan_id];
}
None => {}
}
match &intf.switched_vlan {
Some(switched_vlan) => match switched_vlan.interface_mode {
InterfaceMode::Access => {
if let Some(access_vlan) =
intf.switched_vlan.as_ref().and_then(|x| x.access_vlan)
{
vids.push(access_vlan);
}
}
InterfaceMode::Trunk => {
if let Some(trunk_vlans) =
intf.switched_vlan.as_ref().and_then(|x| x.trunk_vlans.as_ref())
{
vids.extend(trunk_vlans.iter().cloned());
}
}
},
None => (),
}
}
vids
}
/// Returns the [`config::Services`] definition.
///
/// # Errors
///
/// If there is no "services" definition in the configuration, returns a
/// [`error::Config::NotFound`] error.
fn get_services(&self) -> error::Result<&Services> {
self.device()?.services.as_ref().ok_or(error::NetworkManager::CONFIG(
error::Config::NotFound { msg: format!("'services' definition was not found") },
))
}
/// Returns the IP forwarding configuration.
///
/// If IP forwarding is enabled in the config, then this method will return true.
pub fn get_ip_forwarding_state(&self) -> bool {
self.get_services()
.ok()
.and_then(|x| x.ip_forwarding.as_ref())
.map(|i| i.enabled)
.unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::NetworkManager::CONFIG;
use fuchsia_async as fasync;
use std::fs;
fn create_test_config_no_paths() -> Config {
Config::new("/user", "/factory", "/device_schema")
}
fn create_test_config(user_cfg: &str, factory_cfg: &str, device_schema: &str) -> Config {
Config::new(user_cfg, factory_cfg, device_schema)
}
fn create_test_interface() -> Interface {
Interface {
config: InterfaceConfig {
name: "test_eth".to_string(),
interface_type: InterfaceType::IfEthernet,
},
ethernet: None,
tcp_offload: None,
device_id: Some("device_id".to_string()),
oper_state: Some(OperState::Up),
subinterfaces: Some(vec![Subinterface {
admin_state: Some(AdminState::Up),
ipv4: Some(IpAddressConfig {
addresses: vec![IpAddress {
dhcp_client: Some(true),
ip: None,
prefix_length: None,
}],
}),
ipv6: None,
}]),
switched_vlan: None,
routed_vlan: None,
}
}
fn build_full_config() -> DeviceConfig {
DeviceConfig {
device: Device {
acls: None,
interfaces: Some(vec![
Interfaces {
interface: Interface {
config: InterfaceConfig {
name: "wan".to_string(),
interface_type: InterfaceType::IfUplink,
},
oper_state: None,
device_id: Some("test_device_id".to_string()),
ethernet: None,
tcp_offload: None,
routed_vlan: None,
switched_vlan: None,
subinterfaces: Some(vec![Subinterface {
admin_state: Some(AdminState::Up),
ipv4: Some(IpAddressConfig {
addresses: vec![IpAddress {
dhcp_client: Some(false),
ip: Some("127.0.0.1".parse().unwrap()),
prefix_length: Some(32),
}],
}),
ipv6: None,
}]),
},
},
Interfaces {
interface: Interface {
config: InterfaceConfig {
name: "bridge".to_string(),
interface_type: InterfaceType::IfRoutedVlan,
},
oper_state: None,
device_id: Some("routed_vlan".to_string()),
ethernet: None,
tcp_offload: None,
subinterfaces: None,
switched_vlan: None,
routed_vlan: Some(RoutedVlan {
vlan_id: 2,
ipv4: Some(IpAddressConfig {
addresses: vec![IpAddress {
dhcp_client: Some(false),
ip: Some("192.168.0.1".parse().unwrap()),
prefix_length: Some(32),
}],
}),
ipv6: None,
}),
},
},
Interfaces {
interface: Interface {
config: InterfaceConfig {
name: "test_eth".to_string(),
interface_type: InterfaceType::IfEthernet,
},
oper_state: None,
device_id: Some("switched_vlan".to_string()),
ethernet: None,
tcp_offload: None,
subinterfaces: None,
switched_vlan: Some(SwitchedVlan {
interface_mode: InterfaceMode::Access,
access_vlan: Some(2),
trunk_vlans: None,
}),
routed_vlan: None,
},
},
]),
services: Some(Services {
dhcp_server: None,
ip_forwarding: Some(IpForwarding { enabled: true }),
}),
},
}
}
#[test]
fn test_new() {
let user_cfg = "/data/my/user.cfg";
let factory_cfg = "/data/my/factory.cfg";
let device_schema = "/data/my/device_schema.cfg";
let test_config = create_test_config(user_cfg, factory_cfg, device_schema);
assert_eq!(test_config.device_config.is_none(), true);
assert_eq!(test_config.paths.user_config_path, Path::new(user_cfg).to_path_buf());
assert_eq!(test_config.paths.factory_config_path, Path::new(factory_cfg).to_path_buf());
assert_eq!(test_config.paths.device_schema_path, Path::new(device_schema).to_path_buf());
}
#[test]
fn test_get_paths() {
let user_cfg = "/data/my/user.cfg";
let factory_cfg = "/data/my/factory.cfg";
let device_schema = "/data/my/device_schema.cfg";
let test_config = create_test_config(user_cfg, factory_cfg, device_schema);
assert_eq!(test_config.device_config.is_none(), true);
assert_eq!(test_config.user_config_path(), Path::new(user_cfg));
assert_eq!(test_config.factory_config_path(), Path::new(factory_cfg));
assert_eq!(test_config.device_schema_path(), Path::new(device_schema));
assert_eq!(test_config.startup_path(), Path::new(""));
}
#[fasync::run_singlethreaded(test)]
async fn test_load_config_file() {
let test_config = create_test_config_no_paths();
// Missing config should raise an `error::Config::ConfigNotFound`.
let doesntexist = String::from("/doesntexist");
match test_config.try_load_config(Path::new(&doesntexist)).await {
Err(CONFIG(error::Config::ConfigNotFound { path })) => {
assert_eq!(doesntexist, path);
}
Ok(r) => panic!("Got unexpected 'Ok' result: {}", r),
Err(e) => panic!("Got unexpected error result: {}", e),
}
// An invalid config should fail to deserialize.
let invalid_empty = String::from("/pkg/data/invalid_empty.json");
match test_config.try_load_config(Path::new(&invalid_empty)).await {
Err(CONFIG(error::Config::FailedToDeserializeConfig { path, error: _ })) => {
assert_eq!(invalid_empty, path);
}
Ok(r) => panic!("Got unexpected 'Ok' result: {}", r),
Err(e) => panic!("Got unexpected error result: {}", e),
}
// A valid config should deserialize successfully.
let valid_empty = String::from("/pkg/data/valid_empty.json");
let contents = fs::read_to_string(&valid_empty)
.expect(format!("Failed to open testdata file: {}", valid_empty).as_str());
// The expected configuration should deserialize successfully.
let expected_config: Value;
match serde_json::from_str(&contents) {
Ok(j) => expected_config = j,
Err(e) => panic!("Got unexpected error result: {}", e),
}
// The serde_json::Value's should match.
match test_config.try_load_config(Path::new(&valid_empty)).await {
Ok(j) => {
assert_eq!(expected_config, j);
}
Err(e) => panic!("Got unexpected error result: {}", e),
}
}
#[fasync::run_singlethreaded(test)]
async fn test_validate_schema() {
let test_config =
create_test_config("/doesntmatter", "/doesntmatter", "/pkg/data/device_schema.json");
let valid_config = r#"{
"device": {
"interfaces": [
{
"interface": {
"config": {
"name": "wan",
"type": "IF_UPLINK"
},
"subinterfaces": [
{
"ipv4": {
"addresses": [
{
"dhcp_client": false,
"ip": "1.1.1.1",
"prefix_length": 32
}
]
}
}
],
"device_id": "my_test_device"
}
}
]
}
}"#;
let expected_config: Value;
match serde_json::from_str(&valid_config) {
Ok(j) => expected_config = j,
Err(e) => panic!("Got unexpected error result: {}", e),
}
// Make sure that the configuration actually validates.
match test_config.validate_with_schema(&expected_config).await {
Ok(_) => (),
Err(e) => panic!("Got unexpected error result: {}", e),
}
}
#[test]
fn verify_toulouse_factory_config() {
let config_path = "/pkg/data/toulouse_factory_config.json";
let mut contents = String::new();
let mut f = File::open(config_path).unwrap();
f.read_to_string(&mut contents).unwrap();
let _deserialized_config: DeviceConfig = serde_json::from_str(&contents)
.expect(format!("Failed to deserialized {}", config_path).as_ref());
}
#[test]
fn verify_default_factory_config() {
let config_path = "/pkg/data/default_factory_config.json";
let mut contents = String::new();
let mut f = File::open(config_path).unwrap();
f.read_to_string(&mut contents).unwrap();
let _deserialized_config: DeviceConfig = serde_json::from_str(&contents)
.expect(format!("Failed to deserialized {}", config_path).as_ref());
}
#[test]
fn test_validate_interface_types() {
let test_config = create_test_config_no_paths();
let mut intf = create_test_interface();
// If the Interface type is `InterfaceType::IfUplink`, then there must be a 'subinterface'
// defined.
intf.config =
InterfaceConfig { name: "test".to_string(), interface_type: InterfaceType::IfUplink };
match test_config.validate_interface_types(&intf) {
Ok(_) => (),
Err(e) => panic!("Got unexpected error result: {:?}", e),
}
// If the Interface type is `InterfaceType::IfRoutedVlan`, then there must be a
// 'routed_vlan' defined. Anything else should fail.
intf.config = InterfaceConfig {
name: "test".to_string(),
interface_type: InterfaceType::IfRoutedVlan,
};
match test_config.validate_interface_types(&intf) {
Ok(_) => panic!("Got unexpected 'ok' result"),
Err(_) => (),
}
}
#[test]
fn test_validate_interface_config() {
let fake_factory_path = "/fake_factory_path";
let test_config = create_test_config("/doesntmatter", fake_factory_path, "/doesntmatter");
let mut intf = create_test_interface();
// Should pass because an interface must have exactly one configuration.
match test_config.validate_interface_config(&intf) {
Ok(_) => (),
Err(e) => panic!("Got unexpected error result: {:?}", e),
}
// The following should all fail for the same reason as above, exactly one configuration
// per interface.
let routed_vlan = Some(RoutedVlan { vlan_id: 1, ipv4: None, ipv6: None });
let switched_vlan = Some(SwitchedVlan {
interface_mode: InterfaceMode::Access,
access_vlan: None,
trunk_vlans: None,
});
let subinterfaces = Subinterface { admin_state: None, ipv4: None, ipv6: None };
intf.routed_vlan = routed_vlan.clone();
intf.subinterfaces = Some(vec![subinterfaces.clone()]);
intf.switched_vlan = switched_vlan.clone();
let r = test_config.validate_interface_config(&intf);
assert_eq!(true, r.is_err());
intf.routed_vlan = None;
intf.subinterfaces = Some(vec![subinterfaces.clone()]);
intf.switched_vlan = switched_vlan.clone();
let r = test_config.validate_interface_config(&intf);
assert_eq!(true, r.is_err());
intf.routed_vlan = routed_vlan.clone();
intf.subinterfaces = None;
intf.switched_vlan = switched_vlan.clone();
let r = test_config.validate_interface_config(&intf);
assert_eq!(true, r.is_err());
intf.routed_vlan = routed_vlan.clone();
intf.subinterfaces = Some(vec![subinterfaces.clone()]);
intf.switched_vlan = None;
let r = test_config.validate_interface_config(&intf);
assert_eq!(true, r.is_err());
intf.routed_vlan = None;
intf.subinterfaces = None;
intf.switched_vlan = None;
let r = test_config.validate_interface_config(&intf);
assert_eq!(true, r.is_err());
}
#[test]
fn test_validate_subinterfaces() {
let fake_factory_path = "/fake_factory_path";
let test_config = create_test_config("/doesntmatter", fake_factory_path, "/doesntmatter");
// Should not fail because `dhcp_client` is set to true.
let test_subif = vec![Subinterface {
admin_state: Some(AdminState::Up),
ipv4: Some(IpAddressConfig {
addresses: vec![IpAddress {
dhcp_client: Some(true),
ip: None,
prefix_length: None,
}],
}),
ipv6: None,
}];
match test_config.validate_subinterfaces(&test_subif) {
Ok(_) => (),
Err(e) => panic!("Got unexpected error result: {}", e),
}
// Should not fail with dhcp_client set to false, ip and prefix set to valid values.
let test_subif = vec![Subinterface {
admin_state: Some(AdminState::Up),
ipv4: Some(IpAddressConfig {
addresses: vec![IpAddress {
dhcp_client: Some(false),
ip: Some("127.0.0.1".parse().unwrap()),
prefix_length: Some(32),
}],
}),
ipv6: None,
}];
match test_config.validate_subinterfaces(&test_subif) {
Ok(_) => (),
Err(e) => panic!("Got unexpected error result: {}", e),
}
// Should fail with dhcp_client set to false, ip and prefix set to None.
let test_subif = vec![Subinterface {
admin_state: Some(AdminState::Up),
ipv4: Some(IpAddressConfig {
addresses: vec![IpAddress {
dhcp_client: Some(false),
ip: None,
prefix_length: None,
}],
}),
ipv6: None,
}];
match test_config.validate_subinterfaces(&test_subif) {
Err(CONFIG(error::Config::Malformed { msg: _ })) => (),
Err(e) => panic!("Got unexpected error result: {}", e),
Ok(_) => panic!("Got unexpected 'Ok' result!"),
}
// Should not fail with dhcp client set to None, ip, and prefx set to valid values.
let test_subif = vec![Subinterface {
admin_state: Some(AdminState::Up),
ipv4: Some(IpAddressConfig {
addresses: vec![IpAddress {
dhcp_client: None,
ip: Some("127.0.0.1".parse().unwrap()),
prefix_length: Some(32),
}],
}),
ipv6: None,
}];
match test_config.validate_subinterfaces(&test_subif) {
Ok(_) => (),
Err(e) => panic!("Got unexpected error result: {}", e),
}
// Should fail because both dhcp_client and ip/prefix_len are None.
let test_subif = vec![Subinterface {
admin_state: Some(AdminState::Up),
ipv4: Some(IpAddressConfig {
addresses: vec![IpAddress { dhcp_client: None, ip: None, prefix_length: None }],
}),
ipv6: None,
}];
match test_config.validate_subinterfaces(&test_subif) {
Err(CONFIG(error::Config::Malformed { msg: _ })) => (),
Err(e) => panic!("Got unexpected error result: {}", e),
Ok(_) => panic!("Got unexpected 'Ok' result!"),
}
}
#[fasync::run_singlethreaded(test)]
async fn test_final_validation() {
let mut test_config =
create_test_config("/user", "/factory", "/pkg/data/device_schema.json");
let valid_config = r#"{
"device": {
"interfaces": [
{
"interface": {
"config": {
"name": "wan",
"type": "IF_UPLINK"
},
"subinterfaces": [
{
"ipv4": {
"addresses": [
{
"dhcp_client": false,
"ip": "1.1.1.1",
"prefix_length": 32
}
]
}
}
],
"device_id": "my_test_device"
}
}
]
}
}"#;
match serde_json::from_str(&valid_config) {
Ok(j) => test_config.device_config = j,
Err(e) => panic!("Got unexpected error result: {}", e),
}
match test_config.final_validation().await {
Ok(()) => (),
Err(e) => panic!("Got unexpected error result: {:?}", e),
}
let invalid_config = r#"{
"device": {
"interfaces": [
{
"interface": {
"config": {
"name": "wan",
"type": "IF_UPLINK"
},
"subinterfaces": [
{
"ipv4": {
"addresses": [
{
"dhcp_client": false,
"ip": "1.1.1.1",
"prefix_length": 32
}
]
}
}
],
"device_id": "my_test_device"
}
},
{
"interface": {
"config": {
"name": "wan",
"type": "IF_UPLINK"
},
"routed_vlan": {
"vlan_id": 1
},
"subinterfaces": [
{
"ipv4": {
"addresses": [
{
"dhcp_client": false,
"ip": "1.1.1.1",
"prefix_length": 32
}
]
}
}
],
"device_id": "my_test_device"
}
}
]
}
}"#;
match serde_json::from_str(&invalid_config) {
Ok(j) => test_config.device_config = j,
Err(e) => panic!("Got unexpected error result: {}", e),
}
match test_config.final_validation().await {
Ok(_) => panic!("Got unexpected 'ok' result"),
Err(CONFIG(error::Config::FailedToValidateConfig { path: _, error: _ })) => (),
Err(e) => panic!("Got unexpected error result: {}", e),
}
}
#[test]
fn test_get_device() {
let mut test_config = create_test_config_no_paths();
test_config.device_config = Some(build_full_config());
match test_config.device() {
Ok(d) => {
assert_eq!(*d, build_full_config().device);
}
Err(e) => panic!("Got unexpected error result: {}", e),
}
}
#[test]
fn test_get_interfaces() {
let mut test_config = create_test_config_no_paths();
test_config.device_config = Some(build_full_config());
match test_config.interfaces() {
Some(i) => {
assert_eq!(*i, build_full_config().device.interfaces.unwrap());
}
None => panic!("Got unexpected 'None' option"),
}
}
#[test]
fn test_get_interface_by_device_id() {
let mut test_config = create_test_config_no_paths();
test_config.device_config = Some(build_full_config());
match test_config.get_interface_by_device_id("/dev/sys/pci/test_device_id/ethernet") {
Some(i) => {
let intfs = &build_full_config().device.interfaces.unwrap()[0];
assert_eq!(*i, intfs.interface);
}
None => panic!("Got unexpected 'None' option"),
}
match test_config.get_interface_by_device_id("does_not_exist") {
Some(_) => panic!("Got unexpected 'None' option"),
None => (),
}
}
#[test]
fn test_device_id_is_a_wan_uplink() {
let mut test_config = create_test_config_no_paths();
test_config.device_config = Some(build_full_config());
match test_config.device_id_is_a_wan_uplink("/dev/sys/pci/test_device_id/ethernet") {
true => (),
false => panic!("Got unexpected 'false' value"),
}
match test_config.device_id_is_a_wan_uplink("does_not_exist") {
true => panic!("Got unexpected 'false' value"),
false => (),
}
}
#[test]
fn test_get_vlans() {
let mut test_config = create_test_config_no_paths();
test_config.device_config = Some(build_full_config());
let v = test_config.get_vlans("routed_vlan");
assert_eq!(v, vec![2]);
let v = test_config.get_vlans("test_device_id");
let empty_vec: Vec<u16> = Vec::new();
assert_eq!(v, empty_vec);
}
#[test]
fn test_get_services() {
let mut test_config = create_test_config_no_paths();
test_config.device_config = Some(build_full_config());
let services = test_config.get_services();
assert_eq!(*services.unwrap(), build_full_config().device.services.unwrap());
}
#[test]
fn test_get_ip_forwarding() {
let mut test_config = create_test_config_no_paths();
test_config.device_config = Some(build_full_config());
assert_eq!(test_config.get_ip_forwarding_state(), true);
// removing the ip forwarding config should still return the default of false.
test_config.device_config =
Some(DeviceConfig { device: Device { acls: None, interfaces: None, services: None } });
assert_eq!(test_config.get_ip_forwarding_state(), false);
}
#[test]
fn test_set_ip_address_config() {
let test_config = create_test_config_no_paths();
let ipconfig = IpAddress {
dhcp_client: Some(false),
ip: Some("127.0.0.1".parse().unwrap()),
prefix_length: Some(32),
};
let mut properties: lifmgr::LIFProperties =
lifmgr::LIFProperties { enabled: false, dhcp: false, address: None };
test_config.set_ip_address_config(&mut properties, Some(&ipconfig));
// The set_ip_address_config() function should not decide if the LIFProperties are enabled
// or not, that should come from the AdminState in the config. So we shouldn't alter the
// `enabled` field.
assert_eq!(properties.enabled, false);
assert_eq!(properties.dhcp, false);
assert_eq!(
properties.address,
Some(lifmgr::LifIpAddr { address: "127.0.0.1".parse().unwrap(), prefix: 32 })
);
// Make sure DHCP client can be enabled when no static IP configuration is present.
let ipconfig = IpAddress { dhcp_client: Some(true), ip: None, prefix_length: None };
let mut properties: lifmgr::LIFProperties =
lifmgr::LIFProperties { enabled: true, dhcp: false, address: None };
test_config.set_ip_address_config(&mut properties, Some(&ipconfig));
assert_eq!(properties.enabled, true);
assert_eq!(properties.dhcp, true);
assert_eq!(properties.address, None);
// Both DHCP client and static IP cannot be set simultaneously, make sure that DHCP client
// is turned off when both are set.
let ipconfig = IpAddress {
dhcp_client: Some(true),
ip: Some("127.0.0.1".parse().unwrap()),
prefix_length: Some(32),
};
let mut properties: lifmgr::LIFProperties =
lifmgr::LIFProperties { enabled: true, dhcp: false, address: None };
test_config.set_ip_address_config(&mut properties, Some(&ipconfig));
assert_eq!(properties.enabled, true);
assert_eq!(properties.dhcp, false);
assert_eq!(
properties.address,
Some(lifmgr::LifIpAddr { address: "127.0.0.1".parse().unwrap(), prefix: 32 })
);
}
#[test]
fn test_get_ip_address() {
let mut test_config = create_test_config_no_paths();
test_config.device_config = Some(build_full_config());
let intfs = build_full_config().device.interfaces.unwrap();
let subif = &intfs[0].interface.subinterfaces.as_ref().unwrap()[0];
let expected_addr = &subif.ipv4.as_ref().unwrap().addresses[0];
match test_config.get_ip_address(&intfs[0].interface) {
Ok((Some(v4addr), None)) => {
assert_eq!(*v4addr, *expected_addr);
}
Ok(e) => panic!("Got unexpected result pair: {:?}", e),
Err(e) => panic!("Got unexpected result pair: {:?}", e),
}
}
#[test]
fn test_create_wan_properties() {
let mut test_config = create_test_config_no_paths();
test_config.device_config = Some(build_full_config());
match test_config.create_wan_properties("test_device_id") {
Ok(p) => {
assert_eq!(p.enabled, true);
assert_eq!(p.dhcp, false);
assert_eq!(
p.address,
Some(lifmgr::LifIpAddr { address: "127.0.0.1".parse().unwrap(), prefix: 32 })
);
}
Err(e) => panic!("Got unexpected result: {:?}", e),
}
}
#[test]
fn test_get_wan_interface_name() {
let mut test_config = create_test_config_no_paths();
test_config.device_config = Some(build_full_config());
match test_config.get_wan_interface_name("") {
Err(error::NetworkManager::CONFIG(error::Config::NotFound { msg: _ })) => (),
Err(e) => panic!("Got unexpected 'Err' result: {}", e),
Ok(r) => panic!("Got unexpected 'Ok' result: {}", r),
}
match test_config.get_wan_interface_name("bridge") {
Ok(r) => panic!("Got unexpected 'Ok' result: {}", r),
Err(error::NetworkManager::CONFIG(error::Config::NotFound { msg: _ })) => (),
Err(e) => panic!("Got unexpected 'Err' result: {}", e),
}
let expected_name = "wan".to_string();
match test_config.get_wan_interface_name("test_device_id") {
Ok(r) => assert_eq!(r, expected_name),
Err(e) => panic!("Got unexpected 'Err' result: {}", e),
}
}
#[test]
fn test_get_switched_vlan_by_device_id() {
let mut test_config = create_test_config_no_paths();
test_config.device_config = Some(build_full_config());
assert_eq!(
test_config.device_id_is_a_switched_vlan("/dev/foo/whatever/switched_vlan/test"),
true
);
}
#[test]
fn test_get_routed_vlan_interfaces() {
let mut test_config = create_test_config_no_paths();
test_config.device_config = Some(build_full_config());
match test_config.get_routed_vlan_interfaces() {
Some(it) => {
// Turn the iterator into a vector.
let v: Vec<&Interface> = it.collect();
// Check the length is correct.
assert_eq!(v.len(), 1);
// Check that the interface is the correct one.
assert_eq!(v[0].device_id, Some("routed_vlan".to_string()),);
}
None => {
panic!("Unexpected 'None' result: Expecting interface containing the RoutedVlan")
}
}
let mut test_config = create_test_config_no_paths();
test_config.device_config = Some(DeviceConfig {
device: Device {
acls: None,
interfaces: Some(vec![Interfaces {
interface: Interface {
config: InterfaceConfig {
name: "its_a_wan".to_string(),
interface_type: InterfaceType::IfUplink,
},
oper_state: None,
device_id: Some("not_a_routed_vlan".to_string()),
ethernet: None,
tcp_offload: None,
routed_vlan: None,
switched_vlan: None,
subinterfaces: None,
},
}]),
services: None,
},
});
match test_config.get_routed_vlan_interfaces() {
// If there is no RoutedVlan configured, then return an empty iterator.
Some(it) => {
let v: Vec<&Interface> = it.collect();
assert_eq!(v.len(), 0);
}
None => panic!("Unexpected 'None' result: Was expecting an empty iterator"),
};
let mut test_config = create_test_config_no_paths();
test_config.device_config =
Some(DeviceConfig { device: Device { acls: None, interfaces: None, services: None } });
match test_config.get_routed_vlan_interfaces() {
// If there are no interfaces, then should return 'None'.
Some(_) => panic!("Got unexpected 'Some' result: Was expecting 'None'"),
None => (),
};
}
#[test]
fn test_resolve_to_routed_vlan() {
let mut test_config = create_test_config_no_paths();
test_config.device_config = Some(build_full_config());
let switched_vlan = SwitchedVlan {
interface_mode: InterfaceMode::Access,
access_vlan: Some(2),
trunk_vlans: None,
};
let expected_if = test_config.get_routed_vlan_interfaces().unwrap().nth(0).unwrap();
match test_config.resolve_to_routed_vlans(&switched_vlan) {
Ok(r) => assert_eq!(expected_if.routed_vlan.as_ref().unwrap().vlan_id, r.vlan_id),
Err(e) => panic!("Got unexpected 'Error' result: {}", e),
}
let new_sv = SwitchedVlan {
interface_mode: InterfaceMode::Access,
access_vlan: Some(4000),
trunk_vlans: None,
};
let new_config = Some(DeviceConfig {
device: Device {
interfaces: Some(vec![Interfaces {
interface: Interface {
device_id: Some("doesntmatter".to_string()),
config: InterfaceConfig {
name: "doesntmatter".to_string(),
interface_type: InterfaceType::IfEthernet,
},
oper_state: None,
subinterfaces: None,
switched_vlan: Some(new_sv.clone()),
routed_vlan: None,
ethernet: None,
tcp_offload: None,
},
}]),
acls: None,
services: None,
},
});
let mut test_config = create_test_config_no_paths();
test_config.device_config = new_config;
// Should fail because VLAN ID 4000 does not match the routed_vlan configuration.
match test_config.resolve_to_routed_vlans(&new_sv) {
Err(CONFIG(error::Config::NotFound { msg: _ })) => (),
Err(e) => panic!("Got unexpected 'Error' result: {}", e),
Ok(r) => panic!("Got unexpected 'Ok' result: {:?}", r),
}
}
#[test]
fn test_all_ports_have_same_bridge() {
let mut test_config = create_test_config_no_paths();
test_config.device_config = Some(build_full_config());
let sv = SwitchedVlan {
interface_mode: InterfaceMode::Access,
access_vlan: Some(2),
trunk_vlans: None,
};
let vec_of_switched_vlans = vec![&sv, &sv, &sv];
let routed_vlan = test_config
.all_ports_have_same_bridge(vec_of_switched_vlans.into_iter().map(Result::Ok))
.expect("got error");
assert_eq!(routed_vlan.vlan_id, 2);
}
#[test]
fn test_get_bridge_name() {
let mut test_config = create_test_config_no_paths();
test_config.device_config = Some(build_full_config());
let rvi = RoutedVlan { vlan_id: 2, ipv4: None, ipv6: None };
assert_eq!(test_config.get_bridge_name(&rvi), "bridge".to_string());
// bridge name should be the unnamed default.
let rvi = RoutedVlan { vlan_id: 440, ipv4: None, ipv6: None };
assert_eq!(test_config.get_bridge_name(&rvi), "unnamed_bridge".to_string());
}
#[test]
fn test_create_routed_vlan_properties() {
let mut test_config = create_test_config_no_paths();
test_config.device_config = Some(build_full_config());
let rvi = RoutedVlan {
vlan_id: 2,
ipv4: Some(IpAddressConfig {
addresses: vec![IpAddress {
dhcp_client: Some(false),
ip: Some("192.168.0.1".parse().unwrap()),
prefix_length: Some(32),
}],
}),
ipv6: None,
};
match test_config.create_routed_vlan_properties(&rvi) {
Ok(p) => {
assert_eq!(p.enabled, true);
assert_eq!(p.dhcp, false);
assert_eq!(
p.address,
Some(lifmgr::LifIpAddr { address: "192.168.0.1".parse().unwrap(), prefix: 32 })
);
}
Err(e) => panic!("Got unexpected result: {:?}", e),
}
}
}