nmstate/policy/
capture.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use std::collections::HashMap;
4
5use serde::{
6    ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer,
7};
8
9use crate::{ErrorKind, NetworkState, NmstateError};
10
11use super::{
12    iface::{get_iface_match, update_ifaces},
13    json::{get_value_from_json, value_retain_only, value_to_string},
14    route::{get_route_match, update_routes},
15    route_rule::{get_route_rule_match, update_route_rules},
16    token::{parse_str_to_capture_tokens, NetworkCaptureToken},
17};
18
19pub(crate) const PROPERTY_SPLITTER: &str = ".";
20const SORT_CAPTURE_MAX_ROUND: usize = 10;
21
22#[derive(Clone, Debug, Default, PartialEq, Eq)]
23#[non_exhaustive]
24pub struct NetworkCaptureRules {
25    pub cmds: Vec<(String, NetworkCaptureCommand)>,
26}
27
28impl<'de> Deserialize<'de> for NetworkCaptureRules {
29    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
30    where
31        D: Deserializer<'de>,
32    {
33        let map = serde_json::Map::<String, serde_json::Value>::deserialize(
34            deserializer,
35        )?;
36        let mut cmds: Vec<(String, NetworkCaptureCommand)> = Vec::new();
37
38        for (k, v) in map.iter() {
39            if let serde_json::Value::String(s) = v {
40                cmds.push((
41                    k.to_string(),
42                    NetworkCaptureCommand::parse(s.as_str())
43                        .map_err(serde::de::Error::custom)?,
44                ));
45            } else {
46                return Err(serde::de::Error::custom(format!(
47                    "Expecting a string, but got {v}"
48                )));
49            }
50        }
51        log::debug!("Parsed into commands {cmds:?}");
52        Ok(NetworkCaptureRules { cmds })
53    }
54}
55
56impl Serialize for NetworkCaptureRules {
57    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
58    where
59        S: Serializer,
60    {
61        let mut map = serializer.serialize_map(Some(self.cmds.len()))?;
62        for (name, value) in &self.cmds {
63            map.serialize_entry(&name, &value)?;
64        }
65        map.end()
66    }
67}
68
69impl NetworkCaptureRules {
70    pub fn execute(
71        &self,
72        current: &NetworkState,
73    ) -> Result<HashMap<String, NetworkState>, NmstateError> {
74        let mut cmds = self.cmds.clone();
75        sort_captures(&mut cmds)?;
76        let mut ret = HashMap::new();
77        for (var_name, cmd) in cmds.as_slice() {
78            let matched_state = cmd.execute(current, &ret)?;
79            log::debug!("Found match state for {var_name}: {matched_state:?}");
80            ret.insert(var_name.to_string(), matched_state);
81        }
82        Ok(ret)
83    }
84
85    pub(crate) fn is_empty(&self) -> bool {
86        self.cmds.is_empty()
87    }
88}
89
90#[derive(Clone, Debug, Default, PartialEq, Eq)]
91pub struct NetworkCaptureCommand {
92    pub(crate) key: NetworkCaptureToken,
93    pub(crate) key_capture: Option<String>,
94    pub(crate) key_capture_pos: usize,
95    pub(crate) action: NetworkCaptureAction,
96    pub(crate) value: NetworkCaptureToken,
97    pub(crate) value_capture: Option<String>,
98    pub(crate) value_capture_pos: usize,
99    pub(crate) line: String,
100    pub(crate) capture_priority: usize,
101}
102
103impl NetworkCaptureCommand {
104    pub(crate) fn parent_capture(&self) -> Option<&str> {
105        self.key_capture
106            .as_deref()
107            .or(self.value_capture.as_deref())
108    }
109
110    pub(crate) fn parse(line: &str) -> Result<Self, NmstateError> {
111        let line = line
112            .trim()
113            .replace(
114                '\u{A0}', // Non-breaking space
115                " ",
116            )
117            .trim()
118            .to_string();
119
120        let mut ret = Self {
121            line,
122            ..Default::default()
123        };
124        let tokens = parse_str_to_capture_tokens(ret.line.as_str())?;
125        let tokens = tokens.as_slice();
126
127        if let Some(pos) = tokens
128            .iter()
129            .position(|c| matches!(c, NetworkCaptureToken::Pipe(_)))
130        {
131            ret.key_capture = Some(get_input_capture_source(
132                &tokens[..pos],
133                ret.line.as_str(),
134                &tokens[pos],
135            )?);
136            if pos + 1 < tokens.len() {
137                process_tokens_without_pipe(&mut ret, &tokens[pos + 1..])?;
138            }
139        } else {
140            process_tokens_without_pipe(&mut ret, tokens)?;
141        }
142
143        Ok(ret)
144    }
145}
146
147impl Serialize for NetworkCaptureCommand {
148    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
149    where
150        S: Serializer,
151    {
152        serializer.serialize_str(self.line.as_str())
153    }
154}
155
156impl NetworkCaptureCommand {
157    pub(crate) fn execute(
158        &self,
159        current: &NetworkState,
160        captures: &HashMap<String, NetworkState>,
161    ) -> Result<NetworkState, NmstateError> {
162        let input = if let Some(cap_name) = self.key_capture.as_ref() {
163            if let Some(cap) = captures.get(cap_name) {
164                cap.clone()
165            } else {
166                return Err(NmstateError::new_policy_error(
167                    format!("Capture {cap_name} not found"),
168                    self.line.as_str(),
169                    self.key_capture_pos,
170                ));
171            }
172        } else {
173            current.clone()
174        };
175        if self.action == NetworkCaptureAction::None {
176            if let NetworkCaptureToken::Path(keys, _) = &self.key {
177                if keys.is_empty() {
178                    return Ok(NetworkState::new());
179                }
180                let mut input_value =
181                    serde_json::to_value(&input).map_err(|e| {
182                        NmstateError::new(
183                            ErrorKind::Bug,
184                            format!(
185                                "Failed to convert NetworkState {input:?} to \
186                                 serde_json value: {e}"
187                            ),
188                        )
189                    })?;
190                value_retain_only(&mut input_value, keys.as_slice());
191                return NetworkState::deserialize(&input_value).map_err(|e| {
192                    NmstateError::new(
193                        ErrorKind::Bug,
194                        format!(
195                            "Failed to convert NetworkState {input_value:?} \
196                             from serde_json value: {e:?}"
197                        ),
198                    )
199                });
200            } else {
201                // User just want to store full state to a new name
202                return Ok(input);
203            }
204        }
205
206        let value_input = if let Some(cap_name) = self.value_capture.as_ref() {
207            if let Some(cap) = captures.get(cap_name) {
208                cap.clone()
209            } else {
210                return Err(NmstateError::new_policy_error(
211                    format!("Capture {cap_name} not found"),
212                    self.line.as_str(),
213                    self.key_capture_pos,
214                ));
215            }
216        } else {
217            current.clone()
218        };
219        let matching_value =
220            match get_value(&self.value, &value_input, self.line.as_str())? {
221                serde_json::Value::Null => None,
222                v => Some(value_to_string(&v)),
223            };
224        let matching_value_str = matching_value.clone().unwrap_or_default();
225
226        let mut ret = NetworkState::new();
227
228        let (keys, key_pos) =
229            if let NetworkCaptureToken::Path(keys, pos) = &self.key {
230                (keys.as_slice(), pos)
231            } else {
232                return Err(NmstateError::new(
233                    ErrorKind::Bug,
234                    format!(
235                        "The NetworkCaptureCommand.key is not Path but {:?}",
236                        &self.key
237                    ),
238                ));
239            };
240
241        match keys.first().map(String::as_str) {
242            Some("routes") => {
243                ret.routes = match self.action {
244                    NetworkCaptureAction::Equal => get_route_match(
245                        &keys[1..],
246                        matching_value_str.as_str(),
247                        &input,
248                        self.line.as_str(),
249                        key_pos + "routes.".len(),
250                    )?,
251                    NetworkCaptureAction::Replace => update_routes(
252                        &keys[1..],
253                        matching_value.as_deref(),
254                        &input,
255                        self.line.as_str(),
256                        key_pos + "routes.".len(),
257                    )?,
258                    NetworkCaptureAction::None => unreachable!(),
259                }
260            }
261            Some("route-rules") => {
262                ret.rules = match self.action {
263                    NetworkCaptureAction::Equal => get_route_rule_match(
264                        &keys[1..],
265                        matching_value_str.as_str(),
266                        &input,
267                        self.line.as_str(),
268                        key_pos + "route-rules.".len(),
269                    )?,
270                    NetworkCaptureAction::Replace => update_route_rules(
271                        &keys[1..],
272                        matching_value.as_deref(),
273                        &input,
274                        self.line.as_str(),
275                        key_pos + "route-rules.".len(),
276                    )?,
277                    NetworkCaptureAction::None => unreachable!(),
278                }
279            }
280            Some("interfaces") => {
281                ret.interfaces = match self.action {
282                    NetworkCaptureAction::Equal => get_iface_match(
283                        &keys[1..],
284                        matching_value_str.as_str(),
285                        &input,
286                        self.line.as_str(),
287                        key_pos + "interfaces.".len(),
288                    )?,
289                    NetworkCaptureAction::Replace => update_ifaces(
290                        &keys[1..],
291                        matching_value.as_deref(),
292                        &input,
293                        self.line.as_str(),
294                        key_pos + "interfaces.".len(),
295                    )?,
296                    NetworkCaptureAction::None => unreachable!(),
297                }
298            }
299            Some(v) => {
300                return Err(NmstateError::new(
301                    ErrorKind::InvalidArgument,
302                    format!("Unsupported capture keyword '{v}'"),
303                ));
304            }
305            None => {
306                return Err(NmstateError::new(
307                    ErrorKind::InvalidArgument,
308                    "Invalid empty keyword".to_string(),
309                ));
310            }
311        }
312        Ok(ret)
313    }
314}
315
316#[derive(Clone, Copy, Debug, PartialEq, Eq)]
317#[non_exhaustive]
318#[derive(Default)]
319pub enum NetworkCaptureAction {
320    #[default]
321    None,
322    Equal,
323    Replace,
324}
325
326impl std::fmt::Display for NetworkCaptureAction {
327    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328        write!(
329            f,
330            "{}",
331            match self {
332                Self::Equal => "==",
333                Self::Replace => ":=",
334                Self::None => "",
335            }
336        )
337    }
338}
339
340pub(crate) fn get_value(
341    prop_path: &NetworkCaptureToken,
342    state: &NetworkState,
343    line: &str,
344) -> Result<serde_json::Value, NmstateError> {
345    match prop_path {
346        NetworkCaptureToken::Path(prop_path, pos) => {
347            match serde_json::to_value(state)
348                .map_err(|e| {
349                    NmstateError::new(
350                        ErrorKind::Bug,
351                        format!(
352                            "Failed to convert NetworkState {state:?} to \
353                             serde_json value: {e}"
354                        ),
355                    )
356                })?
357                .as_object()
358            {
359                Some(state_value) => {
360                    get_value_from_json(prop_path, state_value, line, *pos)
361                }
362                None => Err(NmstateError::new(
363                    ErrorKind::Bug,
364                    format!(
365                        "Failed to convert NetworkState {state:?} to \
366                         serde_json map",
367                    ),
368                )),
369            }
370        }
371
372        NetworkCaptureToken::Value(v, _) => {
373            Ok(serde_json::Value::String(v.clone()))
374        }
375        NetworkCaptureToken::Null(_) => Ok(serde_json::Value::Null),
376        _ => todo!(),
377    }
378}
379
380fn get_input_capture_source(
381    tokens: &[NetworkCaptureToken],
382    line: &str,
383    pipe_token: &NetworkCaptureToken,
384) -> Result<String, NmstateError> {
385    match tokens.first() {
386        Some(NetworkCaptureToken::Path(path, pos)) => {
387            if path.len() != 2 || path[0] != "capture" {
388                Err(NmstateError::new_policy_error(
389                    "The pipe action should always in format of \
390                     'capture.<capture_name>'"
391                        .to_string(),
392                    line,
393                    *pos,
394                ))
395            } else {
396                Ok(path[1].to_string())
397            }
398        }
399        Some(NetworkCaptureToken::Value(_, pos)) => {
400            Err(NmstateError::new_policy_error(
401                "The pipe action should always in format of \
402                 'capture.<capture_name>'"
403                    .to_string(),
404                line,
405                *pos,
406            ))
407        }
408        Some(token) => Err(NmstateError::new_policy_error(
409            "The pipe action should always in format of \
410             'capture.<capture_name>'"
411                .to_string(),
412            line,
413            token.pos(),
414        )),
415        None => Err(NmstateError::new_policy_error(
416            "The pipe action should always in format of \
417             'capture.<capture_name>'"
418                .to_string(),
419            line,
420            pipe_token.pos(),
421        )),
422    }
423}
424
425fn get_condition_key(
426    tokens: &[NetworkCaptureToken],
427    line: &str,
428    action_token: &NetworkCaptureToken,
429) -> Result<(NetworkCaptureToken, Option<(String, usize)>), NmstateError> {
430    if tokens.len() == 1 {
431        if let Some(NetworkCaptureToken::Path(path, pos)) = tokens.first() {
432            if path.first() == Some(&"capture".to_string()) {
433                if path.len() <= 2 {
434                    return Err(NmstateError::new_policy_error(
435                        "No property path after capture name".to_string(),
436                        line,
437                        *pos,
438                    ));
439                }
440                Ok((
441                    NetworkCaptureToken::Path(
442                        path[2..].to_vec(),
443                        pos + "capture.".len() + path[1].len(),
444                    ),
445                    Some((path[1].to_string(), pos + "capture.".len())),
446                ))
447            } else {
448                Ok((tokens[0].clone(), None))
449            }
450        } else {
451            Err(NmstateError::new_policy_error(
452                "The equal or replace action should always start with \
453                 property path"
454                    .to_string(),
455                line,
456                tokens[0].pos(),
457            ))
458        }
459    } else {
460        Err(NmstateError::new_policy_error(
461            "The equal or replace action should always start with property \
462             path"
463                .to_string(),
464            line,
465            action_token.pos(),
466        ))
467    }
468}
469
470fn get_condition_value(
471    tokens: &[NetworkCaptureToken],
472    line: &str,
473    action_token: &NetworkCaptureToken,
474) -> Result<(NetworkCaptureToken, Option<(String, usize)>), NmstateError> {
475    if tokens.len() != 1 {
476        return Err(NmstateError::new_policy_error(
477            "The equal or replace action should end with single value or \
478             property path"
479                .to_string(),
480            line,
481            if tokens.len() >= 2 {
482                tokens[0].pos()
483            } else {
484                action_token.pos()
485            },
486        ));
487    }
488
489    match tokens[0] {
490        NetworkCaptureToken::Path(ref path, pos) => {
491            Ok(if path.first() == Some(&"capture".to_string()) {
492                if path.len() < 3 {
493                    return Err(NmstateError::new(
494                        ErrorKind::InvalidArgument,
495                        format!(
496                            "When using equal action to match against \
497                             captured data, the correct format should be \
498                             'interfaces.name == \
499                             capture.default-gw.interfaces.0.name', but got: \
500                             {line}"
501                        ),
502                    ));
503                }
504                (
505                    NetworkCaptureToken::Path(
506                        path[2..].to_vec(),
507                        pos + format!("capture.{}.", path[1]).chars().count(),
508                    ),
509                    Some((path[1].to_string(), pos + "capture.".len())),
510                )
511            } else {
512                (tokens[0].clone(), None)
513            })
514        }
515        NetworkCaptureToken::Value(_, _) => Ok((tokens[0].clone(), None)),
516        NetworkCaptureToken::Null(_) => Ok((tokens[0].clone(), None)),
517        _ => Err(NmstateError::new(
518            ErrorKind::InvalidArgument,
519            format!(
520                "The equal action should end with single value or property \
521                 path but got: {line}"
522            ),
523        )),
524    }
525}
526
527fn process_tokens_without_pipe(
528    ret: &mut NetworkCaptureCommand,
529    tokens: &[NetworkCaptureToken],
530) -> Result<(), NmstateError> {
531    let line = ret.line.as_str();
532    if let Some(pos) = tokens
533        .iter()
534        .position(|c| matches!(c, &NetworkCaptureToken::Equal(_)))
535    {
536        if pos + 1 >= tokens.len() {
537            return Err(NmstateError::new_policy_error(
538                "The equal action got no value defined afterwards".to_string(),
539                line,
540                tokens[pos].pos(),
541            ));
542        }
543        ret.action = NetworkCaptureAction::Equal;
544        let (key, key_capture) =
545            get_condition_key(&tokens[..pos], line, &tokens[pos])?;
546        if ret.key_capture.is_none() {
547            if let Some((cap_name, pos)) = key_capture {
548                ret.key_capture = Some(cap_name);
549                ret.key_capture_pos = pos;
550            }
551        }
552        ret.key = key;
553        let (value, value_capture) =
554            get_condition_value(&tokens[pos + 1..], line, &tokens[pos])?;
555        ret.value = value;
556        if let Some((cap_name, pos)) = value_capture {
557            ret.value_capture = Some(cap_name);
558            ret.value_capture_pos = pos;
559        }
560    } else if let Some(pos) = tokens
561        .iter()
562        .position(|c| matches!(c, &NetworkCaptureToken::Replace(_)))
563    {
564        if pos + 1 > tokens.len() {
565            return Err(NmstateError::new_policy_error(
566                "The replace action got no value defined afterwards"
567                    .to_string(),
568                line,
569                tokens[pos].pos(),
570            ));
571        }
572        ret.action = NetworkCaptureAction::Replace;
573        let (key, key_capture) =
574            get_condition_key(&tokens[..pos], line, &tokens[pos])?;
575        if ret.key_capture.is_none() {
576            if let Some((cap_name, pos)) = key_capture {
577                ret.key_capture = Some(cap_name);
578                ret.key_capture_pos = pos;
579            }
580        }
581        ret.key = key;
582        let (value, value_capture) =
583            get_condition_value(&tokens[pos + 1..], line, &tokens[pos])?;
584        ret.value = value;
585        if let Some((cap_name, pos)) = value_capture {
586            ret.value_capture = Some(cap_name);
587            ret.value_capture_pos = pos;
588        }
589    } else if let Some(NetworkCaptureToken::Path(_, _)) = tokens.first() {
590        // User just want to remove all information except the defined one
591        ret.action = NetworkCaptureAction::None;
592        ret.key = tokens[0].clone()
593    }
594    Ok(())
595}
596
597fn sort_captures(
598    cmds: &mut [(String, NetworkCaptureCommand)],
599) -> Result<(), NmstateError> {
600    for _ in 0..SORT_CAPTURE_MAX_ROUND {
601        if set_capture_priority(cmds) {
602            cmds.sort_unstable_by_key(|(_, cmd)| cmd.capture_priority);
603            return Ok(());
604        }
605    }
606    Err(NmstateError::new(
607        ErrorKind::InvalidArgument,
608        format!(
609            "Failed to sort the policy capture in {SORT_CAPTURE_MAX_ROUND} \
610             round of rotation, please order the capture in desire policy by \
611             placing capture before its consumer"
612        ),
613    ))
614}
615
616// Return True if we have all capture_priority are set (not 0).
617// The capture_priority value should be its depend captures' + 1.
618// For independent capture, they are set to 1.
619fn set_capture_priority(cmds: &mut [(String, NetworkCaptureCommand)]) -> bool {
620    let mut ret = true;
621
622    // Vec<(index, (capture_name, capture_priority))>
623    let mut pending_changes: Vec<(usize, (String, usize))> = Vec::new();
624
625    for (index, (cap_name, cmd)) in cmds.iter().enumerate() {
626        let cur_capture_priorities: HashMap<String, usize> = cmds
627            .iter()
628            .filter_map(|(cap_name, cmd)| {
629                if cmd.capture_priority != 0 {
630                    Some((cap_name.to_string(), cmd.capture_priority))
631                } else {
632                    None
633                }
634            })
635            .chain(pending_changes.iter().map(|(_, (cap_name, priority))| {
636                (cap_name.to_string(), *priority)
637            }))
638            .collect();
639        if let Some(dep_name) = cmd.parent_capture() {
640            if let Some(parent_priority) = cur_capture_priorities.get(dep_name)
641            {
642                pending_changes
643                    .push((index, (cap_name.to_string(), parent_priority + 1)))
644            } else {
645                ret = false;
646            }
647        } else {
648            pending_changes.push((index, (cap_name.to_string(), 1)));
649        }
650    }
651
652    for (index, (_, capture_priority)) in pending_changes {
653        cmds[index].1.capture_priority = capture_priority;
654    }
655    ret
656}