use std::collections::{hash_map::Entry, HashMap, HashSet};
use std::convert::TryFrom;
use serde::{Deserialize, Serialize};
use crate::{ip::is_ipv6_addr, ErrorKind, InterfaceIpAddr, NmstateError};
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(deny_unknown_fields)]
pub struct RouteRules {
#[serde(skip_serializing_if = "Option::is_none")]
pub config: Option<Vec<RouteRuleEntry>>,
}
impl RouteRules {
pub fn new() -> Self {
Self::default()
}
pub(crate) fn validate(&self) -> Result<(), NmstateError> {
if let Some(rules) = self.config.as_ref() {
for rule in rules.iter().filter(|r| !r.is_absent()) {
rule.validate()?;
}
}
Ok(())
}
pub fn verify(&self, current: &Self) -> Result<(), NmstateError> {
if let Some(rules) = self.config.as_ref() {
let empty_vec: Vec<RouteRuleEntry> = Vec::new();
let cur_rules = match current.config.as_deref() {
Some(c) => c,
None => empty_vec.as_slice(),
};
for rule in rules.iter().filter(|r| !r.is_absent()) {
if !cur_rules.iter().any(|r| rule.is_match(r)) {
let e = NmstateError::new(
ErrorKind::VerificationError,
format!(
"Desired route rule {:?} not found after apply",
rule
),
);
log::error!("{}", e);
return Err(e);
}
}
for absent_rule in rules.iter().filter(|r| r.is_absent()) {
if rules
.iter()
.any(|r| (!r.is_absent()) && absent_rule.is_match(r))
{
continue;
}
if let Some(cur_rule) =
cur_rules.iter().find(|r| absent_rule.is_match(r))
{
let e = NmstateError::new(
ErrorKind::VerificationError,
format!(
"Desired absent route rule {:?} still found \
after apply: {:?}",
absent_rule, cur_rule
),
);
log::error!("{}", e);
return Err(e);
}
}
}
Ok(())
}
pub(crate) fn gen_rule_changed_table_ids(
&self,
current: &Self,
) -> HashMap<u32, Vec<RouteRuleEntry>> {
let mut ret: HashMap<u32, Vec<RouteRuleEntry>> = HashMap::new();
let cur_rules_index = current
.config
.as_ref()
.map(|c| create_rule_index_by_table_id(c.as_slice()))
.unwrap_or_default();
let des_rules_index = self
.config
.as_ref()
.map(|c| create_rule_index_by_table_id(c.as_slice()))
.unwrap_or_default();
let mut table_ids_in_desire: HashSet<u32> =
des_rules_index.keys().copied().collect();
let absent_rules = flat_absent_rule(
self.config.as_deref().unwrap_or(&[]),
current.config.as_deref().unwrap_or(&[]),
);
for absent_rule in &absent_rules {
if let Some(i) = absent_rule.table_id {
log::debug!(
"Route table is impacted by absent rule {:?}",
absent_rule
);
table_ids_in_desire.insert(i);
}
}
for table_id in &table_ids_in_desire {
if let Some(cur_rules) = cur_rules_index.get(table_id) {
ret.insert(
*table_id,
cur_rules
.as_slice()
.iter()
.map(|r| (*r).clone())
.collect::<Vec<RouteRuleEntry>>(),
);
}
}
for absent_rule in &absent_rules {
if let Some(table_id) = absent_rule.table_id.as_ref() {
if let Some(rules) = ret.get_mut(table_id) {
rules.retain(|r| !absent_rule.is_match(r));
}
}
}
for (table_id, desire_rules) in des_rules_index.iter() {
let new_rules = desire_rules
.iter()
.map(|r| (*r).clone())
.collect::<Vec<RouteRuleEntry>>();
match ret.entry(*table_id) {
Entry::Occupied(o) => {
o.into_mut().extend(new_rules);
}
Entry::Vacant(v) => {
v.insert(new_rules);
}
};
}
for desire_rules in ret.values_mut() {
desire_rules.sort_unstable();
desire_rules.dedup();
}
ret
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum RouteRuleState {
Absent,
}
impl Default for RouteRuleState {
fn default() -> Self {
Self::Absent
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
#[serde(deny_unknown_fields)]
pub struct RouteRuleEntry {
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<RouteRuleState>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ip_from: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ip_to: Option<String>,
#[serde(
skip_serializing_if = "Option::is_none",
default,
deserialize_with = "crate::deserializer::option_i64_or_string"
)]
pub priority: Option<i64>,
#[serde(
skip_serializing_if = "Option::is_none",
rename = "route-table",
default,
deserialize_with = "crate::deserializer::option_u32_or_string"
)]
pub table_id: Option<u32>,
}
impl RouteRuleEntry {
pub const USE_DEFAULT_PRIORITY: i64 = -1;
pub const USE_DEFAULT_ROUTE_TABLE: u32 = 0;
pub const DEFAULR_ROUTE_TABLE_ID: u32 = 254;
pub fn new() -> Self {
Self::default()
}
pub(crate) fn validate(&self) -> Result<(), NmstateError> {
if self.ip_from.is_none() && self.ip_to.is_none() {
let e = NmstateError::new(
ErrorKind::InvalidArgument,
format!(
"Neither ip-from or ip-to is defined in route rule {:?}",
self
),
);
log::error!("{}", e);
return Err(e);
}
Ok(())
}
fn is_absent(&self) -> bool {
matches!(self.state, Some(RouteRuleState::Absent))
}
fn is_match(&self, other: &Self) -> bool {
if let Some(ip_from) = self.ip_from.as_deref() {
let ip_from = if !ip_from.contains('/') {
match InterfaceIpAddr::try_from(ip_from) {
Ok(ref i) => i.into(),
Err(e) => {
log::error!("{}", e);
return false;
}
}
} else {
ip_from.to_string()
};
if other.ip_from != Some(ip_from) {
return false;
}
}
if let Some(ip_to) = self.ip_to.as_deref() {
let ip_to = if !ip_to.contains('/') {
match InterfaceIpAddr::try_from(ip_to) {
Ok(ref i) => i.into(),
Err(e) => {
log::error!("{}", e);
return false;
}
}
} else {
ip_to.to_string()
};
if other.ip_to != Some(ip_to) {
return false;
}
}
if self.priority.is_some()
&& self.priority != Some(RouteRuleEntry::USE_DEFAULT_PRIORITY)
&& self.priority != other.priority
{
return false;
}
if self.table_id.is_some()
&& self.table_id != Some(RouteRuleEntry::USE_DEFAULT_ROUTE_TABLE)
&& self.table_id != other.table_id
{
return false;
}
true
}
fn sort_key(&self) -> (bool, bool, u32, &str, &str, i64) {
(
!matches!(self.state, Some(RouteRuleState::Absent)),
{
if let Some(ip_from) = self.ip_from.as_ref() {
!is_ipv6_addr(ip_from.as_str())
} else if let Some(ip_to) = self.ip_to.as_ref() {
!is_ipv6_addr(ip_to.as_str())
} else {
log::warn!(
"Neither ip-from nor ip-to \
is defined, treating it a IPv4 route rule"
);
true
}
},
self.table_id
.unwrap_or(RouteRuleEntry::USE_DEFAULT_ROUTE_TABLE),
self.ip_from.as_deref().unwrap_or(""),
self.ip_to.as_deref().unwrap_or(""),
self.priority
.unwrap_or(RouteRuleEntry::USE_DEFAULT_PRIORITY),
)
}
}
impl PartialEq for RouteRuleEntry {
fn eq(&self, other: &Self) -> bool {
self.sort_key() == other.sort_key()
}
}
impl Ord for RouteRuleEntry {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.sort_key().cmp(&other.sort_key())
}
}
impl Eq for RouteRuleEntry {}
impl PartialOrd for RouteRuleEntry {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
fn create_rule_index_by_table_id(
rules: &[RouteRuleEntry],
) -> HashMap<u32, Vec<&RouteRuleEntry>> {
let mut ret: HashMap<u32, Vec<&RouteRuleEntry>> = HashMap::new();
for rule in rules {
if rule.is_absent() {
continue;
}
let table_id = match rule.table_id {
Some(RouteRuleEntry::USE_DEFAULT_ROUTE_TABLE) | None => {
RouteRuleEntry::DEFAULR_ROUTE_TABLE_ID
}
Some(i) => i,
};
match ret.entry(table_id) {
Entry::Occupied(o) => {
o.into_mut().push(rule);
}
Entry::Vacant(v) => {
v.insert(vec![rule]);
}
};
}
ret
}
fn flat_absent_rule(
desire_rules: &[RouteRuleEntry],
cur_rules: &[RouteRuleEntry],
) -> Vec<RouteRuleEntry> {
let mut ret: Vec<RouteRuleEntry> = Vec::new();
for absent_rule in desire_rules.iter().filter(|r| r.is_absent()) {
if absent_rule.table_id.is_none() {
for cur_rule in cur_rules {
if absent_rule.is_match(cur_rule) {
let mut new_absent_rule = absent_rule.clone();
new_absent_rule.table_id = cur_rule.table_id;
ret.push(new_absent_rule);
}
}
} else {
ret.push(absent_rule.clone());
}
}
ret
}