1use 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]
15pub struct BridgePortVlanConfig {
17 #[serde(
18 skip_serializing_if = "Option::is_none",
19 default,
20 deserialize_with = "crate::deserializer::option_bool_or_string"
21 )]
22 pub enable_native: Option<bool>,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 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 pub tag: Option<u16>,
35 #[serde(
36 skip_serializing_if = "Option::is_none",
37 serialize_with = "bridge_trunk_tags_serialize"
38 )]
39 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,
169 #[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 Id(u16),
194 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 pub min: u16,
295 #[serde(deserialize_with = "crate::deserializer::u16_or_string")]
296 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}