1use std::collections::{BTreeMap, HashSet};
4
5use serde::{Deserialize, Serialize};
6
7use crate::{ErrorKind, InterfaceType, MergedInterfaces, NmstateError};
8
9#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
10#[non_exhaustive]
11#[serde(deny_unknown_fields)]
12pub struct OvnConfiguration {
26 #[serde(
27 rename = "bridge-mappings",
28 skip_serializing_if = "Option::is_none"
29 )]
30 pub bridge_mappings: Option<Vec<OvnBridgeMapping>>,
31}
32
33impl OvnConfiguration {
34 const SEPARATOR: &'static str = ",";
35
36 pub(crate) fn is_none(&self) -> bool {
37 self.bridge_mappings.is_none()
38 }
39
40 pub(crate) fn sanitize(&mut self) -> Result<(), NmstateError> {
41 self.sanitize_unique_localnet_keys()?;
42 if let Some(maps) = self.bridge_mappings.as_deref_mut() {
43 for map in maps {
44 map.sanitize()?;
45 }
46 }
47 Ok(())
48 }
49
50 fn sanitize_unique_localnet_keys(&self) -> Result<(), NmstateError> {
51 if let Some(maps) = self.bridge_mappings.as_deref() {
52 let localnet_keys: Vec<&str> =
53 maps.iter().map(|m| m.localnet.as_str()).collect();
54 for map in maps {
55 if localnet_keys
56 .iter()
57 .filter(|k| k == &&map.localnet.as_str())
58 .count()
59 > 1
60 {
61 return Err(NmstateError::new(
62 ErrorKind::InvalidArgument,
63 format!(
64 "Found duplicate `localnet` key {}",
65 map.localnet
66 ),
67 ));
68 }
69 }
70 }
71 Ok(())
72 }
73
74 pub(crate) fn validate_local_bridge(
75 &self,
76 merged_ifaces: &MergedInterfaces,
77 ) -> Result<(), NmstateError> {
78 if let Some(maps) = self.bridge_mappings.as_ref() {
79 for map in maps
80 .iter()
81 .filter(|map| map.state != Some(OvnBridgeMappingState::Absent))
82 {
83 if let Some(br_name) = map.bridge.as_deref() {
84 if let Some(ovs_br_iface) = merged_ifaces
85 .get_iface(br_name, InterfaceType::OvsBridge)
86 {
87 if ovs_br_iface.merged.is_absent() {
88 return Err(NmstateError::new(
89 ErrorKind::InvalidArgument,
90 format!(
91 "The OVS bridge {br_name} used for OVN \
92 localnet {} is marked as absent",
93 map.localnet
94 ),
95 ));
96 }
97 } else {
98 return Err(NmstateError::new(
99 ErrorKind::InvalidArgument,
100 format!(
101 "There is no OVS bridge holding name desired \
102 by OVN bridge {br_name} localnet {}",
103 map.localnet
104 ),
105 ));
106 }
107 }
108 }
109 }
110 Ok(())
111 }
112
113 pub(crate) fn to_ovsdb_external_id_value(&self) -> Option<String> {
114 if let Some(maps) = self.bridge_mappings.as_ref() {
115 let mut maps = maps.clone();
116 maps.dedup();
117 maps.sort_unstable();
118 if maps.is_empty() {
119 None
120 } else {
121 Some(
122 maps.as_slice()
123 .iter()
124 .map(|map| map.to_string())
125 .collect::<Vec<String>>()
126 .join(Self::SEPARATOR),
127 )
128 }
129 } else {
130 None
131 }
132 }
133}
134
135impl TryFrom<&str> for OvnConfiguration {
136 type Error = NmstateError;
137
138 fn try_from(maps_str: &str) -> Result<Self, NmstateError> {
139 let mut maps = Vec::new();
140 for map_str in maps_str.split(Self::SEPARATOR) {
141 if !map_str.is_empty() {
142 maps.push(map_str.try_into()?);
143 }
144 }
145 maps.dedup();
146 maps.sort_unstable();
147
148 Ok(Self {
149 bridge_mappings: if maps.is_empty() { None } else { Some(maps) },
150 })
151 }
152}
153
154#[derive(Clone, Debug, Default, PartialEq, Eq)]
163pub(crate) struct MergedOvnConfiguration {
164 pub(crate) desired: OvnConfiguration,
165 pub(crate) current: OvnConfiguration,
166 pub(crate) ovsdb_ext_id_value: Option<String>,
167}
168
169impl MergedOvnConfiguration {
170 pub(crate) fn new(
174 desired: OvnConfiguration,
175 current: OvnConfiguration,
176 merged_ifaces: &MergedInterfaces,
177 ) -> Result<Self, NmstateError> {
178 let mut desired = desired;
179 desired.sanitize()?;
180
181 desired.validate_local_bridge(merged_ifaces)?;
182
183 let empty_vec: Vec<OvnBridgeMapping> = Vec::new();
184 let deleted_localnets: HashSet<&str> = desired
185 .bridge_mappings
186 .as_ref()
187 .unwrap_or(&empty_vec)
188 .iter()
189 .filter_map(|m| {
190 if m.is_absent() {
191 Some(m.localnet.as_str())
192 } else {
193 None
194 }
195 })
196 .collect();
197 let mut desired_ovn_maps: BTreeMap<&str, &str> = BTreeMap::new();
198
199 for cur_map in current.bridge_mappings.as_ref().unwrap_or(&empty_vec) {
200 if let Some(cur_br) = cur_map.bridge.as_deref() {
201 if !deleted_localnets.contains(&cur_map.localnet.as_str()) {
202 desired_ovn_maps.insert(cur_map.localnet.as_str(), cur_br);
203 }
204 }
205 }
206 for des_map in desired.bridge_mappings.as_ref().unwrap_or(&empty_vec) {
207 if let Some(des_br) = des_map.bridge.as_deref() {
208 if !des_map.is_absent() {
209 desired_ovn_maps.insert(des_map.localnet.as_str(), des_br);
210 }
211 }
212 }
213
214 let ovsdb_ext_id_value = OvnConfiguration {
215 bridge_mappings: Some(
216 desired_ovn_maps
217 .iter()
218 .map(|(k, v)| OvnBridgeMapping {
219 localnet: k.to_string(),
220 bridge: Some(v.to_string()),
221 ..Default::default()
222 })
223 .collect(),
224 ),
225 }
226 .to_ovsdb_external_id_value();
227
228 Ok(Self {
229 desired,
230 current,
231 ovsdb_ext_id_value,
232 })
233 }
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
237#[non_exhaustive]
238pub struct OvnBridgeMapping {
239 pub localnet: String,
240 #[serde(skip_serializing_if = "Option::is_none")]
241 pub state: Option<OvnBridgeMappingState>,
244 #[serde(skip_serializing_if = "Option::is_none")]
245 pub bridge: Option<String>,
246}
247
248impl PartialOrd for OvnBridgeMapping {
250 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
251 Some(self.cmp(other))
252 }
253}
254
255impl Ord for OvnBridgeMapping {
257 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
258 self.sort_key().cmp(&other.sort_key())
259 }
260}
261
262impl TryFrom<&str> for OvnBridgeMapping {
263 type Error = NmstateError;
264
265 fn try_from(map_str: &str) -> Result<Self, NmstateError> {
266 let items: Vec<&str> = map_str.split(Self::SEPARATOR).collect();
267 if items.len() != 2 || items[1].is_empty() || items[0].is_empty() {
268 Err(NmstateError::new(
269 ErrorKind::InvalidArgument,
270 format!(
271 "Cannot convert {map_str} to OvnBridgeMapping, expected \
272 format is `<localnet>{}<bridge>`",
273 Self::SEPARATOR
274 ),
275 ))
276 } else {
277 Ok(Self {
278 localnet: items[0].to_string(),
279 bridge: Some(items[1].to_string()),
280 ..Default::default()
281 })
282 }
283 }
284}
285
286impl OvnBridgeMapping {
287 const SEPARATOR: &'static str = ":";
288
289 pub(crate) fn is_absent(&self) -> bool {
290 self.state == Some(OvnBridgeMappingState::Absent)
291 }
292
293 fn sort_key(&self) -> (bool, &str, Option<&str>) {
294 (
295 !self.is_absent(),
297 self.localnet.as_str(),
298 self.bridge.as_deref(),
299 )
300 }
301
302 pub fn sanitize(&mut self) -> Result<(), NmstateError> {
303 if !self.is_absent() {
304 self.state = None;
305 }
306 if (!self.is_absent()) && self.bridge.is_none() {
307 return Err(NmstateError::new(
308 ErrorKind::InvalidArgument,
309 format!(
310 "mapping for `localnet` key {} missing the `bridge` \
311 attribute",
312 self.localnet
313 ),
314 ));
315 }
316 Ok(())
317 }
318}
319
320impl std::fmt::Display for OvnBridgeMapping {
321 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
322 if let Some(bridge) = self.bridge.as_ref() {
323 write!(f, "{}:{}", self.localnet, bridge,)
324 } else {
325 write!(f, "",)
326 }
327 }
328}
329
330#[derive(
331 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize,
332)]
333#[serde(rename_all = "lowercase", deny_unknown_fields)]
334#[non_exhaustive]
335pub enum OvnBridgeMappingState {
336 #[deprecated(since = "2.2.17", note = "No state means present")]
337 Present,
338 Absent,
339}