1use 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}', " ",
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 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 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
616fn set_capture_priority(cmds: &mut [(String, NetworkCaptureCommand)]) -> bool {
620 let mut ret = true;
621
622 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}