nmstate/ifaces/
bridge_vlan.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use std::collections::hash_map::Entry;
4use std::collections::HashMap;
5
6use serde::{
7    ser::SerializeTuple, Deserialize, Deserializer, Serialize, Serializer,
8};
9
10use crate::{ErrorKind, NmstateError};
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
13#[serde(rename_all = "kebab-case", deny_unknown_fields)]
14#[non_exhaustive]
15/// Bridge VLAN filtering configuration
16pub struct BridgePortVlanConfig {
17    #[serde(
18        skip_serializing_if = "Option::is_none",
19        default,
20        deserialize_with = "crate::deserializer::option_bool_or_string"
21    )]
22    /// Enable native VLAN.
23    /// Deserialize and serialize from/to `enable-native`.
24    pub enable_native: Option<bool>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    /// Bridge VLAN filtering mode
27    pub mode: Option<BridgePortVlanMode>,
28    #[serde(
29        skip_serializing_if = "Option::is_none",
30        default,
31        deserialize_with = "crate::deserializer::option_u16_or_string"
32    )]
33    /// VLAN Tag for native VLAN.
34    pub tag: Option<u16>,
35    #[serde(
36        skip_serializing_if = "Option::is_none",
37        serialize_with = "bridge_trunk_tags_serialize"
38    )]
39    /// Trunk tags.
40    /// Deserialize and serialize from/to `trunk-tags`.
41    pub trunk_tags: Option<Vec<BridgePortTrunkTag>>,
42}
43
44impl BridgePortVlanConfig {
45    pub fn new() -> Self {
46        Self::default()
47    }
48
49    pub(crate) fn is_changed(&self, current: &Self) -> bool {
50        (self.enable_native.is_some()
51            && self.enable_native != current.enable_native)
52            || (self.mode.is_some() && self.mode != current.mode)
53            || (self.tag.is_some() && self.tag != current.tag)
54            || (self.trunk_tags.is_some()
55                && self.trunk_tags != current.trunk_tags)
56    }
57
58    pub(crate) fn is_empty(&self) -> bool {
59        self.enable_native.is_none()
60            && self.mode.is_none()
61            && self.tag.is_none()
62            && self.trunk_tags.is_none()
63    }
64
65    pub(crate) fn sort_trunk_tags(&mut self) {
66        if let Some(trunk_tags) = self.trunk_tags.as_mut() {
67            trunk_tags.sort_unstable_by(|tag_a, tag_b| match (tag_a, tag_b) {
68                (BridgePortTrunkTag::Id(a), BridgePortTrunkTag::Id(b)) => {
69                    a.cmp(b)
70                }
71                _ => {
72                    log::warn!(
73                        "Please call flatten_vlan_ranges() before \
74                         sort_port_vlans()"
75                    );
76                    std::cmp::Ordering::Equal
77                }
78            })
79        }
80    }
81
82    pub(crate) fn flatten_vlan_ranges(&mut self) {
83        if let Some(trunk_tags) = &self.trunk_tags {
84            let mut new_trunk_tags = Vec::new();
85            for trunk_tag in trunk_tags {
86                match trunk_tag {
87                    BridgePortTrunkTag::Id(_) => {
88                        new_trunk_tags.push(trunk_tag.clone())
89                    }
90                    BridgePortTrunkTag::IdRange(range) => {
91                        for i in range.min..range.max + 1 {
92                            new_trunk_tags.push(BridgePortTrunkTag::Id(i));
93                        }
94                    }
95                };
96            }
97            self.trunk_tags = Some(new_trunk_tags);
98        }
99    }
100
101    pub(crate) fn sanitize(
102        &self,
103        is_desired: bool,
104    ) -> Result<(), NmstateError> {
105        if is_desired {
106            if self.mode == Some(BridgePortVlanMode::Trunk)
107                && self.tag.is_some()
108                && self.tag != Some(0)
109                && self.enable_native != Some(true)
110            {
111                return Err(NmstateError::new(
112                    ErrorKind::InvalidArgument,
113                    "Bridge VLAN filtering `tag` cannot be use in trunk mode \
114                     without `enable-native`"
115                        .to_string(),
116                ));
117            }
118
119            if self.mode == Some(BridgePortVlanMode::Access)
120                && self.enable_native == Some(true)
121            {
122                return Err(NmstateError::new(
123                    ErrorKind::InvalidArgument,
124                    "Bridge VLAN filtering `enable-native: true` cannot be \
125                     set in access mode"
126                        .to_string(),
127                ));
128            }
129
130            if self.mode == Some(BridgePortVlanMode::Access) {
131                if let Some(tags) = self.trunk_tags.as_ref() {
132                    if !tags.is_empty() {
133                        return Err(NmstateError::new(
134                            ErrorKind::InvalidArgument,
135                            "Bridge VLAN filtering access mode cannot have \
136                             trunk-tags defined"
137                                .to_string(),
138                        ));
139                    }
140                }
141            }
142
143            if self.mode == Some(BridgePortVlanMode::Trunk)
144                && self.trunk_tags.is_none()
145            {
146                return Err(NmstateError::new(
147                    ErrorKind::InvalidArgument,
148                    "Bridge VLAN filtering trunk mode cannot have empty \
149                     trunk-tags"
150                        .to_string(),
151                ));
152            }
153            if let Some(tags) = self.trunk_tags.as_ref() {
154                validate_overlap_trunk_tags(tags)?;
155            }
156        }
157
158        Ok(())
159    }
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(rename_all = "kebab-case")]
164#[non_exhaustive]
165#[derive(Default)]
166pub enum BridgePortVlanMode {
167    /// Trunk mode
168    Trunk,
169    /// Access mode
170    #[default]
171    Access,
172}
173
174impl std::fmt::Display for BridgePortVlanMode {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        write!(
177            f,
178            "{}",
179            match self {
180                Self::Trunk => "trunk",
181                Self::Access => "access",
182            }
183        )
184    }
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
188#[serde(rename_all = "kebab-case")]
189#[non_exhaustive]
190pub enum BridgePortTrunkTag {
191    #[serde(deserialize_with = "crate::deserializer::u16_or_string")]
192    /// Single VLAN trunk ID
193    Id(u16),
194    /// VLAN trunk ID range
195    IdRange(BridgePortVlanRange),
196}
197
198impl std::fmt::Display for BridgePortTrunkTag {
199    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200        match self {
201            Self::Id(d) => write!(f, "id={d}"),
202            Self::IdRange(range) => {
203                write!(f, "id-range=[{},{}]", range.min, range.max)
204            }
205        }
206    }
207}
208
209impl<'de> Deserialize<'de> for BridgePortTrunkTag {
210    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
211    where
212        D: Deserializer<'de>,
213    {
214        let v = serde_json::Value::deserialize(deserializer)?;
215        if let Some(id) = v.get("id") {
216            if let Some(id) = id.as_str() {
217                Ok(Self::Id(id.parse::<u16>().map_err(|e| {
218                    serde::de::Error::custom(format!(
219                        "Failed to parse BridgePortTrunkTag id {id} as u16: \
220                         {e}"
221                    ))
222                })?))
223            } else if let Some(id) = id.as_u64() {
224                Ok(Self::Id(u16::try_from(id).map_err(|e| {
225                    serde::de::Error::custom(format!(
226                        "Failed to parse BridgePortTrunkTag id {id} as u16: \
227                         {e}"
228                    ))
229                })?))
230            } else {
231                Err(serde::de::Error::custom(format!(
232                    "The id of BridgePortTrunkTag should be unsigned 16 bits \
233                     integer, but got {v}"
234                )))
235            }
236        } else if let Some(id_range) = v.get("id-range") {
237            Ok(Self::IdRange(
238                BridgePortVlanRange::deserialize(id_range)
239                    .map_err(serde::de::Error::custom)?,
240            ))
241        } else {
242            Err(serde::de::Error::custom(format!(
243                "BridgePortTrunkTag only support 'id' or 'id-range', but got \
244                 {v}"
245            )))
246        }
247    }
248}
249
250fn bridge_trunk_tags_serialize<S>(
251    tags: &Option<Vec<BridgePortTrunkTag>>,
252    serializer: S,
253) -> Result<S::Ok, S::Error>
254where
255    S: Serializer,
256{
257    if let Some(tags) = tags {
258        let mut serial_list = serializer.serialize_tuple(tags.len())?;
259        for tag in tags {
260            match tag {
261                BridgePortTrunkTag::Id(id) => {
262                    let mut map = HashMap::new();
263                    map.insert("id", id);
264                    serial_list.serialize_element(&map)?;
265                }
266                BridgePortTrunkTag::IdRange(id_range) => {
267                    let mut map = HashMap::new();
268                    map.insert("id-range", id_range);
269                    serial_list.serialize_element(&map)?;
270                }
271            }
272        }
273        serial_list.end()
274    } else {
275        serializer.serialize_none()
276    }
277}
278
279impl BridgePortTrunkTag {
280    pub fn get_vlan_tag_range(&self) -> (u16, u16) {
281        match self {
282            Self::Id(min) => (*min, *min),
283            Self::IdRange(range) => (range.min, range.max),
284        }
285    }
286}
287
288#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
289#[non_exhaustive]
290#[serde(deny_unknown_fields)]
291pub struct BridgePortVlanRange {
292    #[serde(deserialize_with = "crate::deserializer::u16_or_string")]
293    /// Minimum VLAN ID(included).
294    pub min: u16,
295    #[serde(deserialize_with = "crate::deserializer::u16_or_string")]
296    /// Maximum VLAN ID(included).
297    pub max: u16,
298}
299
300fn validate_overlap_trunk_tags(
301    tags: &[BridgePortTrunkTag],
302) -> Result<(), NmstateError> {
303    let mut found: HashMap<u16, &BridgePortTrunkTag> = HashMap::new();
304    for tag in tags {
305        match tag {
306            BridgePortTrunkTag::Id(d) => match found.entry(*d) {
307                Entry::Occupied(o) => {
308                    let existing_tag = o.get();
309                    return Err(NmstateError::new(
310                        ErrorKind::InvalidArgument,
311                        format!(
312                            "Bridge VLAN trunk tag {tag} is overlapping with \
313                             other tag {existing_tag}"
314                        ),
315                    ));
316                }
317                Entry::Vacant(v) => {
318                    v.insert(tag);
319                }
320            },
321
322            BridgePortTrunkTag::IdRange(range) => {
323                for i in range.min..range.max + 1 {
324                    match found.entry(i) {
325                        Entry::Occupied(o) => {
326                            let existing_tag = o.get();
327                            return Err(NmstateError::new(
328                                ErrorKind::InvalidArgument,
329                                format!(
330                                    "Bridge VLAN trunk tag {tag} is \
331                                     overlapping with other tag {existing_tag}"
332                                ),
333                            ));
334                        }
335                        Entry::Vacant(v) => {
336                            v.insert(tag);
337                        }
338                    }
339                }
340            }
341        }
342    }
343    Ok(())
344}