1use std::collections::{BTreeMap, HashSet};
4
5use serde::{Deserialize, Serialize};
6
7use crate::{ErrorKind, 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 to_ovsdb_external_id_value(&self) -> Option<String> {
75 if let Some(maps) = self.bridge_mappings.as_ref() {
76 let mut maps = maps.clone();
77 maps.dedup();
78 maps.sort_unstable();
79 if maps.is_empty() {
80 None
81 } else {
82 Some(
83 maps.as_slice()
84 .iter()
85 .map(|map| map.to_string())
86 .collect::<Vec<String>>()
87 .join(Self::SEPARATOR),
88 )
89 }
90 } else {
91 None
92 }
93 }
94}
95
96impl TryFrom<&str> for OvnConfiguration {
97 type Error = NmstateError;
98
99 fn try_from(maps_str: &str) -> Result<Self, NmstateError> {
100 let mut maps = Vec::new();
101 for map_str in maps_str.split(Self::SEPARATOR) {
102 if !map_str.is_empty() {
103 maps.push(map_str.try_into()?);
104 }
105 }
106 maps.dedup();
107 maps.sort_unstable();
108
109 Ok(Self {
110 bridge_mappings: if maps.is_empty() { None } else { Some(maps) },
111 })
112 }
113}
114
115#[derive(Clone, Debug, Default, PartialEq, Eq)]
124pub(crate) struct MergedOvnConfiguration {
125 pub(crate) desired: OvnConfiguration,
126 pub(crate) current: OvnConfiguration,
127 pub(crate) ovsdb_ext_id_value: Option<String>,
128}
129
130impl MergedOvnConfiguration {
131 pub(crate) fn new(
135 desired: OvnConfiguration,
136 current: OvnConfiguration,
137 ) -> Result<Self, NmstateError> {
138 let mut desired = desired;
139 desired.sanitize()?;
140
141 let empty_vec: Vec<OvnBridgeMapping> = Vec::new();
142 let deleted_localnets: HashSet<&str> = desired
143 .bridge_mappings
144 .as_ref()
145 .unwrap_or(&empty_vec)
146 .iter()
147 .filter_map(|m| {
148 if m.is_absent() {
149 Some(m.localnet.as_str())
150 } else {
151 None
152 }
153 })
154 .collect();
155 let mut desired_ovn_maps: BTreeMap<&str, &str> = BTreeMap::new();
156
157 for cur_map in current.bridge_mappings.as_ref().unwrap_or(&empty_vec) {
158 if let Some(cur_br) = cur_map.bridge.as_deref() {
159 if !deleted_localnets.contains(&cur_map.localnet.as_str()) {
160 desired_ovn_maps.insert(cur_map.localnet.as_str(), cur_br);
161 }
162 }
163 }
164 for des_map in desired.bridge_mappings.as_ref().unwrap_or(&empty_vec) {
165 if let Some(des_br) = des_map.bridge.as_deref() {
166 if !des_map.is_absent() {
167 desired_ovn_maps.insert(des_map.localnet.as_str(), des_br);
168 }
169 }
170 }
171
172 let ovsdb_ext_id_value = OvnConfiguration {
173 bridge_mappings: Some(
174 desired_ovn_maps
175 .iter()
176 .map(|(k, v)| OvnBridgeMapping {
177 localnet: k.to_string(),
178 bridge: Some(v.to_string()),
179 ..Default::default()
180 })
181 .collect(),
182 ),
183 }
184 .to_ovsdb_external_id_value();
185
186 Ok(Self {
187 desired,
188 current,
189 ovsdb_ext_id_value,
190 })
191 }
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
195#[non_exhaustive]
196pub struct OvnBridgeMapping {
197 pub localnet: String,
198 #[serde(skip_serializing_if = "Option::is_none")]
199 pub state: Option<OvnBridgeMappingState>,
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub bridge: Option<String>,
204}
205
206impl PartialOrd for OvnBridgeMapping {
208 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
209 Some(self.cmp(other))
210 }
211}
212
213impl Ord for OvnBridgeMapping {
215 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
216 self.sort_key().cmp(&other.sort_key())
217 }
218}
219
220impl TryFrom<&str> for OvnBridgeMapping {
221 type Error = NmstateError;
222
223 fn try_from(map_str: &str) -> Result<Self, NmstateError> {
224 let items: Vec<&str> = map_str.split(Self::SEPARATOR).collect();
225 if items.len() != 2 || items[1].is_empty() || items[0].is_empty() {
226 Err(NmstateError::new(
227 ErrorKind::InvalidArgument,
228 format!(
229 "Cannot convert {map_str} to OvnBridgeMapping, \
230 expected format is `<localnet>{}<bridge>`",
231 Self::SEPARATOR
232 ),
233 ))
234 } else {
235 Ok(Self {
236 localnet: items[0].to_string(),
237 bridge: Some(items[1].to_string()),
238 ..Default::default()
239 })
240 }
241 }
242}
243
244impl OvnBridgeMapping {
245 const SEPARATOR: &'static str = ":";
246
247 pub(crate) fn is_absent(&self) -> bool {
248 self.state == Some(OvnBridgeMappingState::Absent)
249 }
250
251 fn sort_key(&self) -> (bool, &str, Option<&str>) {
252 (
253 !self.is_absent(),
255 self.localnet.as_str(),
256 self.bridge.as_deref(),
257 )
258 }
259
260 pub fn sanitize(&mut self) -> Result<(), NmstateError> {
261 if !self.is_absent() {
262 self.state = None;
263 }
264 if (!self.is_absent()) && self.bridge.is_none() {
265 return Err(NmstateError::new(
266 ErrorKind::InvalidArgument,
267 format!(
268 "mapping for `localnet` key {} missing the \
269 `bridge` attribute",
270 self.localnet
271 ),
272 ));
273 }
274 Ok(())
275 }
276}
277
278impl std::fmt::Display for OvnBridgeMapping {
279 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
280 if let Some(bridge) = self.bridge.as_ref() {
281 write!(f, "{}:{}", self.localnet, bridge,)
282 } else {
283 write!(f, "",)
284 }
285 }
286}
287
288#[derive(
289 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize,
290)]
291#[serde(rename_all = "lowercase", deny_unknown_fields)]
292#[non_exhaustive]
293pub enum OvnBridgeMappingState {
294 #[deprecated(since = "2.2.17", note = "No state means present")]
295 Present,
296 Absent,
297}