1use std::{
4 collections::{BTreeMap, HashMap, hash_map::Iter},
5 marker::PhantomData,
6};
7
8use serde::{
9 Deserialize, Deserializer, Serialize, Serializer, de,
10 de::{MapAccess, Visitor},
11};
12
13use crate::MergedInterface;
14
15const ETHTOOL_FEATURE_CLI_ALIAS: [(&str, &str); 17] = [
16 ("rx", "rx-checksum"),
17 ("rx-checksumming", "rx-checksum"),
18 ("ufo", "tx-udp-fragmentation"),
19 ("gso", "tx-generic-segmentation"),
20 ("generic-segmentation-offload", "tx-generic-segmentation"),
21 ("gro", "rx-gro"),
22 ("generic-receive-offload", "rx-gro"),
23 ("lro", "rx-lro"),
24 ("large-receive-offload", "rx-lro"),
25 ("rxvlan", "rx-vlan-hw-parse"),
26 ("rx-vlan-offload", "rx-vlan-hw-parse"),
27 ("txvlan", "tx-vlan-hw-insert"),
28 ("tx-vlan-offload", "tx-vlan-hw-insert"),
29 ("ntuple", "rx-ntuple-filter"),
30 ("ntuple-filters", "rx-ntuple-filter"),
31 ("rxhash", "rx-hashing"),
32 ("receive-hashing", "rx-hashing"),
33];
34
35#[derive(Deserialize, Debug, Eq, PartialEq, Clone, Default)]
36#[serde(from = "HashMap<String, bool>")]
37#[non_exhaustive]
38pub struct EthtoolFeatureConfig {
39 data: HashMap<String, bool>,
40}
41
42impl EthtoolFeatureConfig {
43 pub fn remove(&mut self, key: &str) -> Option<bool> {
44 self.data.remove(key)
45 }
46
47 pub fn insert(&mut self, key: String, value: bool) -> Option<bool> {
48 self.data.insert(key, value)
49 }
50
51 pub fn get(&self, key: &str) -> Option<&bool> {
52 self.data.get(key)
53 }
54}
55
56impl<'a> IntoIterator for &'a EthtoolFeatureConfig {
57 type Item = (&'a String, &'a bool);
58 type IntoIter = Iter<'a, String, bool>;
59
60 fn into_iter(self) -> Self::IntoIter {
61 self.data.iter()
62 }
63}
64
65impl From<HashMap<String, bool>> for EthtoolFeatureConfig {
66 fn from(data: HashMap<String, bool>) -> Self {
67 Self { data }
68 }
69}
70
71impl From<EthtoolFeatureConfig> for HashMap<String, bool> {
72 fn from(c: EthtoolFeatureConfig) -> Self {
73 c.data
74 }
75}
76
77impl Serialize for EthtoolFeatureConfig {
78 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
79 where
80 S: Serializer,
81 {
82 let ordered: BTreeMap<_, _> = self.data.iter().collect();
83 ordered.serialize(serializer)
84 }
85}
86
87#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone, Default)]
88#[non_exhaustive]
89#[serde(deny_unknown_fields)]
90pub struct EthtoolConfig {
121 #[serde(skip_serializing_if = "Option::is_none")]
122 pub pause: Option<EthtoolPauseConfig>,
124 #[serde(
125 skip_serializing_if = "Option::is_none",
126 default,
127 deserialize_with = "parse_ethtool_feature"
128 )]
129 pub feature: Option<EthtoolFeatureConfig>,
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub coalesce: Option<EthtoolCoalesceConfig>,
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub ring: Option<EthtoolRingConfig>,
138 #[serde(skip_serializing_if = "Option::is_none")]
140 pub fec: Option<EthtoolFecConfig>,
141}
142
143impl EthtoolConfig {
144 pub fn new() -> Self {
145 Self::default()
146 }
147
148 pub(crate) fn apply_feature_alias(&mut self) {
150 if let Some(features) = self.feature.as_mut() {
151 for (cli_alias, kernel_name) in ETHTOOL_FEATURE_CLI_ALIAS {
152 if let Some(v) = features.remove(cli_alias) {
153 features.insert(kernel_name.to_string(), v);
154 }
155 }
156 }
157 }
158
159 pub(crate) fn sanitize(&mut self, is_desired: bool) {
160 if let Some(fec_conf) = self.fec.as_mut() {
161 fec_conf.sanitize(is_desired)
162 }
163 }
164}
165
166#[derive(
167 Serialize, Deserialize, Debug, Eq, PartialEq, Clone, Default, Copy,
168)]
169#[non_exhaustive]
170#[serde(deny_unknown_fields)]
171pub struct EthtoolPauseConfig {
172 #[serde(
173 skip_serializing_if = "Option::is_none",
174 default,
175 deserialize_with = "crate::deserializer::option_bool_or_string"
176 )]
177 pub rx: Option<bool>,
178 #[serde(
179 skip_serializing_if = "Option::is_none",
180 default,
181 deserialize_with = "crate::deserializer::option_bool_or_string"
182 )]
183 pub tx: Option<bool>,
184 #[serde(
185 skip_serializing_if = "Option::is_none",
186 default,
187 deserialize_with = "crate::deserializer::option_bool_or_string"
188 )]
189 pub autoneg: Option<bool>,
190}
191
192impl EthtoolPauseConfig {
193 pub fn new() -> Self {
194 Self::default()
195 }
196}
197
198#[derive(
199 Serialize, Deserialize, Debug, Eq, PartialEq, Clone, Default, Copy,
200)]
201#[serde(rename_all = "kebab-case", deny_unknown_fields)]
202#[non_exhaustive]
203pub struct EthtoolCoalesceConfig {
204 #[serde(
205 skip_serializing_if = "Option::is_none",
206 default,
207 deserialize_with = "crate::deserializer::option_bool_or_string"
208 )]
209 pub adaptive_rx: Option<bool>,
211 #[serde(
212 skip_serializing_if = "Option::is_none",
213 default,
214 deserialize_with = "crate::deserializer::option_bool_or_string"
215 )]
216 pub adaptive_tx: Option<bool>,
218 #[serde(
219 skip_serializing_if = "Option::is_none",
220 default,
221 deserialize_with = "crate::deserializer::option_u32_or_string"
222 )]
223 pub pkt_rate_high: Option<u32>,
225 #[serde(
226 skip_serializing_if = "Option::is_none",
227 default,
228 deserialize_with = "crate::deserializer::option_u32_or_string"
229 )]
230 pub pkt_rate_low: Option<u32>,
232 #[serde(
233 skip_serializing_if = "Option::is_none",
234 default,
235 deserialize_with = "crate::deserializer::option_u32_or_string"
236 )]
237 pub rx_frames: Option<u32>,
239 #[serde(
240 skip_serializing_if = "Option::is_none",
241 default,
242 deserialize_with = "crate::deserializer::option_u32_or_string"
243 )]
244 pub rx_frames_high: Option<u32>,
246 #[serde(
247 skip_serializing_if = "Option::is_none",
248 default,
249 deserialize_with = "crate::deserializer::option_u32_or_string"
250 )]
251 pub rx_frames_irq: Option<u32>,
253 #[serde(
254 skip_serializing_if = "Option::is_none",
255 default,
256 deserialize_with = "crate::deserializer::option_u32_or_string"
257 )]
258 pub rx_frames_low: Option<u32>,
260 #[serde(
261 skip_serializing_if = "Option::is_none",
262 default,
263 deserialize_with = "crate::deserializer::option_u32_or_string"
264 )]
265 pub rx_usecs: Option<u32>,
267 #[serde(
268 skip_serializing_if = "Option::is_none",
269 default,
270 deserialize_with = "crate::deserializer::option_u32_or_string"
271 )]
272 pub rx_usecs_high: Option<u32>,
274 #[serde(
275 skip_serializing_if = "Option::is_none",
276 default,
277 deserialize_with = "crate::deserializer::option_u32_or_string"
278 )]
279 pub rx_usecs_irq: Option<u32>,
281 #[serde(
282 skip_serializing_if = "Option::is_none",
283 default,
284 deserialize_with = "crate::deserializer::option_u32_or_string"
285 )]
286 pub rx_usecs_low: Option<u32>,
288 #[serde(
289 skip_serializing_if = "Option::is_none",
290 default,
291 deserialize_with = "crate::deserializer::option_u32_or_string"
292 )]
293 pub sample_interval: Option<u32>,
295 #[serde(
296 skip_serializing_if = "Option::is_none",
297 default,
298 deserialize_with = "crate::deserializer::option_u32_or_string"
299 )]
300 pub stats_block_usecs: Option<u32>,
302 #[serde(
303 skip_serializing_if = "Option::is_none",
304 default,
305 deserialize_with = "crate::deserializer::option_u32_or_string"
306 )]
307 pub tx_frames: Option<u32>,
309 #[serde(
310 skip_serializing_if = "Option::is_none",
311 default,
312 deserialize_with = "crate::deserializer::option_u32_or_string"
313 )]
314 pub tx_frames_high: Option<u32>,
316 #[serde(
317 skip_serializing_if = "Option::is_none",
318 default,
319 deserialize_with = "crate::deserializer::option_u32_or_string"
320 )]
321 pub tx_frames_irq: Option<u32>,
323 #[serde(
324 skip_serializing_if = "Option::is_none",
325 default,
326 deserialize_with = "crate::deserializer::option_u32_or_string"
327 )]
328 pub tx_frames_low: Option<u32>,
330 #[serde(
331 skip_serializing_if = "Option::is_none",
332 default,
333 deserialize_with = "crate::deserializer::option_u32_or_string"
334 )]
335 pub tx_usecs: Option<u32>,
337 #[serde(
338 skip_serializing_if = "Option::is_none",
339 default,
340 deserialize_with = "crate::deserializer::option_u32_or_string"
341 )]
342 pub tx_usecs_high: Option<u32>,
344 #[serde(
345 skip_serializing_if = "Option::is_none",
346 default,
347 deserialize_with = "crate::deserializer::option_u32_or_string"
348 )]
349 pub tx_usecs_irq: Option<u32>,
351 #[serde(
352 skip_serializing_if = "Option::is_none",
353 default,
354 deserialize_with = "crate::deserializer::option_u32_or_string"
355 )]
356 pub tx_usecs_low: Option<u32>,
358}
359
360impl EthtoolCoalesceConfig {
361 pub fn new() -> Self {
362 Self::default()
363 }
364}
365
366#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone, Default)]
367#[serde(rename_all = "kebab-case", deny_unknown_fields)]
368#[non_exhaustive]
369pub struct EthtoolRingConfig {
370 #[serde(
371 skip_serializing_if = "Option::is_none",
372 default,
373 deserialize_with = "crate::deserializer::option_u32_or_string"
374 )]
375 pub rx: Option<u32>,
376 #[serde(
377 skip_serializing_if = "Option::is_none",
378 default,
379 deserialize_with = "crate::deserializer::option_u32_or_string"
380 )]
381 pub rx_max: Option<u32>,
383 #[serde(
384 skip_serializing_if = "Option::is_none",
385 default,
386 deserialize_with = "crate::deserializer::option_u32_or_string"
387 )]
388 pub rx_jumbo: Option<u32>,
390 #[serde(
391 skip_serializing_if = "Option::is_none",
392 default,
393 deserialize_with = "crate::deserializer::option_u32_or_string"
394 )]
395 pub rx_jumbo_max: Option<u32>,
397 #[serde(
398 skip_serializing_if = "Option::is_none",
399 default,
400 deserialize_with = "crate::deserializer::option_u32_or_string"
401 )]
402 pub rx_mini: Option<u32>,
404 #[serde(
405 skip_serializing_if = "Option::is_none",
406 default,
407 deserialize_with = "crate::deserializer::option_u32_or_string"
408 )]
409 pub rx_mini_max: Option<u32>,
411 #[serde(
412 skip_serializing_if = "Option::is_none",
413 default,
414 deserialize_with = "crate::deserializer::option_u32_or_string"
415 )]
416 pub tx: Option<u32>,
417 #[serde(
418 skip_serializing_if = "Option::is_none",
419 default,
420 deserialize_with = "crate::deserializer::option_u32_or_string"
421 )]
422 pub tx_max: Option<u32>,
424}
425
426impl EthtoolRingConfig {
427 pub fn new() -> Self {
428 Self::default()
429 }
430}
431
432fn parse_ethtool_feature<'de, D>(
433 deserializer: D,
434) -> Result<Option<EthtoolFeatureConfig>, D::Error>
435where
436 D: Deserializer<'de>,
437{
438 struct FeatureVisitor(PhantomData<fn() -> Option<EthtoolFeatureConfig>>);
439
440 impl<'de> Visitor<'de> for FeatureVisitor {
441 type Value = Option<EthtoolFeatureConfig>;
442
443 fn expecting(
444 &self,
445 formatter: &mut std::fmt::Formatter,
446 ) -> std::fmt::Result {
447 formatter.write_str("Need to hash map of String:bool")
448 }
449
450 fn visit_map<M>(
451 self,
452 mut access: M,
453 ) -> Result<Option<EthtoolFeatureConfig>, M::Error>
454 where
455 M: MapAccess<'de>,
456 {
457 let mut ret =
458 HashMap::with_capacity(access.size_hint().unwrap_or(0));
459
460 while let Some((key, value)) =
461 access.next_entry::<String, serde_json::Value>()?
462 {
463 match value {
464 serde_json::Value::Bool(b) => {
465 ret.insert(key, b);
466 }
467 serde_json::Value::String(b)
468 if b.to_lowercase().as_str() == "false"
469 || b.as_str() == "0" =>
470 {
471 ret.insert(key, false);
472 }
473 serde_json::Value::String(b)
474 if b.to_lowercase().as_str() == "true"
475 || b.as_str() == "1" =>
476 {
477 ret.insert(key, true);
478 }
479 _ => {
480 return Err(de::Error::custom(
481 "Invalid feature value, should be boolean",
482 ));
483 }
484 }
485 }
486
487 Ok(Some(ret.into()))
488 }
489 }
490
491 deserializer.deserialize_any(FeatureVisitor(PhantomData))
492}
493
494impl MergedInterface {
495 pub(crate) fn post_inter_ifaces_process_ethtool(&mut self) {
496 if let Some(ethtool_conf) = self
497 .for_apply
498 .as_mut()
499 .map(|i| i.base_iface_mut())
500 .and_then(|b| b.ethtool.as_mut())
501 {
502 ethtool_conf.apply_feature_alias();
503 }
504 if let Some(ethtool_conf) = self
505 .for_verify
506 .as_mut()
507 .map(|i| i.base_iface_mut())
508 .and_then(|b| b.ethtool.as_mut())
509 {
510 ethtool_conf.apply_feature_alias();
511 }
512 }
513}
514
515#[derive(
516 Serialize, Deserialize, Debug, Eq, PartialEq, Clone, Default, Copy,
517)]
518#[non_exhaustive]
519#[serde(deny_unknown_fields)]
520pub struct EthtoolFecConfig {
521 #[serde(
523 skip_serializing_if = "Option::is_none",
524 default,
525 deserialize_with = "crate::deserializer::option_bool_or_string"
526 )]
527 pub auto: Option<bool>,
528 #[serde(skip_serializing_if = "Option::is_none")]
532 pub mode: Option<EthtoolFecMode>,
533}
534
535impl EthtoolFecConfig {
536 pub(crate) fn sanitize(&mut self, is_desired: bool) {
537 if is_desired
538 && self.auto == Some(true)
539 && let Some(mode) = self.mode.take()
540 {
541 log::info!(
542 "Ignoring ethtool fec mode setting {mode} because auto enabled"
543 );
544 }
545 }
546}
547
548#[derive(
549 Serialize, Deserialize, Debug, Eq, PartialEq, Clone, Default, Copy,
550)]
551#[non_exhaustive]
552#[serde(rename_all = "kebab-case", deny_unknown_fields)]
553pub enum EthtoolFecMode {
554 #[default]
555 Off,
556 Rs,
557 Baser,
558 Llrs,
559}
560
561impl std::fmt::Display for EthtoolFecMode {
562 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
563 write!(
564 f,
565 "{}",
566 match self {
567 Self::Off => "off",
568 Self::Rs => "rs",
569 Self::Baser => "baser",
570 Self::Llrs => "llrs",
571 }
572 )
573 }
574}