nmstate 2.2.60

Library for networking management in a declarative manner
Documentation
// SPDX-License-Identifier: Apache-2.0

use std::collections::{HashMap, HashSet};

use serde::{Deserialize, Serialize};

use crate::{
    BaseInterface, ErrorKind, Interface, InterfaceType, MergedInterface,
    MergedInterfaces, NmstateError, Routes,
};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
/// Linux kernel Virtual Routing and Forwarding(VRF) interface. The example
/// yaml output of a [crate::NetworkState] with a VRF interface would be:
/// ```yml
/// interfaces:
/// - name: vrf0
///   type: vrf
///   state: up
///   mac-address: 42:6C:4A:0B:A3:C0
///   mtu: 65575
///   min-mtu: 1280
///   max-mtu: 65575
///   wait-ip: any
///   ipv4:
///     enabled: false
///   ipv6:
///     enabled: false
///   accept-all-mac-addresses: false
///   vrf:
///     port:
///     - eth1
///     - eth2
///     route-table-id: 100
/// ```
pub struct VrfInterface {
    #[serde(flatten)]
    pub base: BaseInterface,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vrf: Option<VrfConfig>,
}

impl Default for VrfInterface {
    fn default() -> Self {
        Self {
            base: BaseInterface {
                iface_type: InterfaceType::Vrf,
                ..Default::default()
            },
            vrf: None,
        }
    }
}

impl VrfInterface {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn ports(&self) -> Option<Vec<&str>> {
        self.vrf
            .as_ref()
            .and_then(|vrf_conf| vrf_conf.port.as_ref())
            .map(|ports| ports.as_slice().iter().map(|p| p.as_str()).collect())
    }

    pub(crate) fn sanitize(
        &mut self,
        is_desired: bool,
    ) -> Result<(), NmstateError> {
        // Ignoring the changes of MAC address of VRF as it is a layer 3
        // interface.
        if is_desired && let Some(mac) = self.base.mac_address.as_ref() {
            log::warn!(
                "Ignoring MAC address {mac} of VRF interface {} as it is a \
                 layer 3(IP) interface",
                self.base.name.as_str()
            );
        }
        self.base.mac_address = None;
        if self.base.accept_all_mac_addresses == Some(false) {
            self.base.accept_all_mac_addresses = None;
        }
        // Sort ports
        if let Some(ports) = self.vrf.as_mut().and_then(|c| c.port.as_mut()) {
            ports.sort();
        }

        Ok(())
    }

    pub(crate) fn merge_table_id(
        &mut self,
        current: Option<&Interface>,
    ) -> Result<(), NmstateError> {
        if self
            .vrf
            .as_ref()
            .and_then(|v| v.table_id)
            .unwrap_or_default()
            == 0
        {
            if let Some(cur_table_id) =
                if let Some(Interface::Vrf(vrf_iface)) = current {
                    vrf_iface.vrf.as_ref().and_then(|v| v.table_id)
                } else {
                    None
                }
            {
                if let Some(vrf_conf) = self.vrf.as_mut() {
                    vrf_conf.table_id = Some(cur_table_id);
                }
            } else {
                let e = NmstateError::new(
                    ErrorKind::InvalidArgument,
                    format!(
                        "Route table ID undefined or 0 is not allowed for new \
                         VRF interface {}",
                        self.base.name
                    ),
                );
                log::error!("{e}");
                return Err(e);
            }
        }
        Ok(())
    }

    pub(crate) fn change_port_name(
        &mut self,
        origin_name: &str,
        new_name: String,
    ) {
        if let Some(port_name) = self
            .vrf
            .as_mut()
            .and_then(|vrf_conf| vrf_conf.port.as_mut())
            .and_then(|ports| {
                ports
                    .iter_mut()
                    .find(|port_name| port_name.as_str() == origin_name)
            })
        {
            *port_name = new_name;
        }
    }

    pub(crate) fn remove_port(&mut self, port_to_remove: &str) {
        if let Some(ports) = self
            .vrf
            .as_mut()
            .and_then(|vrf_conf| vrf_conf.port.as_mut())
        {
            ports.retain(|port_name| port_name != port_to_remove);
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[non_exhaustive]
#[serde(deny_unknown_fields)]
pub struct VrfConfig {
    #[serde(alias = "ports", skip_serializing_if = "Option::is_none")]
    /// Port list.
    /// Deserialize and serialize from/to `port`.
    /// Also deserialize from `ports`.
    pub port: Option<Vec<String>>,
    #[serde(
        rename = "route-table-id",
        default,
        skip_serializing_if = "Option::is_none",
        deserialize_with = "crate::deserializer::option_u32_or_string"
    )]
    /// Route table ID of this VRF interface.
    /// Deserialize and serialize from/to `route-table-id`.
    pub table_id: Option<u32>,
}

impl MergedInterface {
    // Merge table ID from current if desired table ID is 0
    pub(crate) fn post_inter_ifaces_process_vrf(
        &mut self,
    ) -> Result<(), NmstateError> {
        if !self.merged.is_absent() {
            if let Some(Interface::Vrf(apply_iface)) = self.for_apply.as_mut() {
                apply_iface.merge_table_id(self.current.as_ref())?;
            }
            if let Some(Interface::Vrf(verify_iface)) = self.for_verify.as_mut()
            {
                verify_iface.merge_table_id(self.current.as_ref())?;
            }
        }
        Ok(())
    }
}

impl Routes {
    pub(crate) fn resolve_vrf_name(
        &mut self,
        merged_ifaces: &MergedInterfaces,
    ) -> Result<(), NmstateError> {
        let vrf_names_down: HashSet<&str> = merged_ifaces
            .kernel_ifaces
            .values()
            .filter(|merged_iface| merged_iface.merged.is_down())
            .filter_map(|merged_iface| {
                if merged_iface.merged.iface_type() == InterfaceType::Vrf {
                    Some(merged_iface.merged.name())
                } else {
                    None
                }
            })
            .collect();
        let vrf_names_absent: HashSet<&str> = merged_ifaces
            .kernel_ifaces
            .values()
            .filter(|merged_iface| merged_iface.merged.is_absent())
            .filter_map(|merged_iface| {
                if merged_iface.merged.iface_type() == InterfaceType::Vrf {
                    Some(merged_iface.merged.name())
                } else {
                    None
                }
            })
            .collect();
        let vrf_name_to_table_id: HashMap<&str, u32> = merged_ifaces
            .kernel_ifaces
            .values()
            .filter(|merged_iface| merged_iface.merged.is_up())
            .filter_map(|merged_iface| {
                if let Interface::Vrf(vrf_iface) = &merged_iface.merged {
                    vrf_iface.vrf.as_ref().and_then(|v| v.table_id).map(
                        |table_id| (vrf_iface.base.name.as_str(), table_id),
                    )
                } else {
                    None
                }
            })
            .collect();
        if let Some(config_routes) = self.config.as_mut() {
            for rt in config_routes.iter_mut() {
                let vrf_name = if let Some(n) = rt.vrf_name.as_deref() {
                    n
                } else {
                    continue;
                };
                if vrf_names_down.contains(vrf_name) {
                    return Err(NmstateError::new(
                        ErrorKind::InvalidArgument,
                        format!(
                            "VRF defined in Route '{rt}' is marked as 'state: \
                             down'"
                        ),
                    ));
                }
                if vrf_names_absent.contains(vrf_name) {
                    return Err(NmstateError::new(
                        ErrorKind::InvalidArgument,
                        format!(
                            "VRF defined in Route '{rt}' is marked as 'state: \
                             absent'"
                        ),
                    ));
                }
                let table_id = if let Some(t) =
                    vrf_name_to_table_id.get(vrf_name)
                {
                    *t
                } else {
                    return Err(NmstateError::new(
                        ErrorKind::InvalidArgument,
                        format!("VRF defined in Route '{rt}' does not exist"),
                    ));
                };
                if let Some(des_table_id) = rt.table_id
                    && des_table_id != table_id
                {
                    return Err(NmstateError::new(
                        ErrorKind::InvalidArgument,
                        format!(
                            "Route '{rt}' has both table id and VRF name \
                             defined, but desired table ID is {} while table \
                             ID for VRF {} is {}",
                            des_table_id, vrf_name, table_id
                        ),
                    ));
                }
                rt.table_id = Some(table_id);
            }
        }
        Ok(())
    }
}