nmstate/
ovn.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use 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)]
12/// Global OVN bridge mapping configuration. Example yaml output of
13/// [crate::NetworkState]:
14/// ```yml
15/// ---
16/// ovn:
17///   bridge-mappings:
18///   - localnet: tenantblue
19///     bridge: ovsbr1
20///     state: present
21///   - localnet: tenantred
22///     bridge: ovsbr1
23///     state: absent
24/// ```
25pub 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// The OVN is just syntax sugar wrapping single entry in ovsdb `external_ids`
155// section.
156// Before sending to backends for applying, we store it into
157// `MergedOvsDbGlobalConfig` as normal `external_ids` entry.
158// When receiving from backend for querying, we use
159// `NetworkState::isolate_ovn()` to isolate this `external_ids` entry
160// into `OvnConfiguration`.
161// For verification, we are treating it as normal property without extracting.
162#[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    // Partial editing for ovn:
171    //  * Merge desire with current and do overriding.
172    //  * To remove a particular ovn-bridge-mapping, do `state: absent`
173    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    /// When set to `state: absent`, will delete the existing
242    /// `localnet` mapping.
243    pub state: Option<OvnBridgeMappingState>,
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub bridge: Option<String>,
246}
247
248// For Ord
249impl PartialOrd for OvnBridgeMapping {
250    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
251        Some(self.cmp(other))
252    }
253}
254
255// For Vec::sort_unstable()
256impl 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            // We want absent mapping listed before others
296            !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}