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, 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 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// The OVN is just syntax sugar wrapping single entry in ovsdb `external_ids`
116// section.
117// Before sending to backends for applying, we store it into
118// `MergedOvsDbGlobalConfig` as normal `external_ids` entry.
119// When receiving from backend for querying, we use
120// `NetworkState::isolate_ovn()` to isolate this `external_ids` entry
121// into `OvnConfiguration`.
122// For verification, we are treating it as normal property without extracting.
123#[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    // Partial editing for ovn:
132    //  * Merge desire with current and do overriding.
133    //  * To remove a particular ovn-bridge-mapping, do `state: absent`
134    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    /// When set to `state: absent`, will delete the existing
200    /// `localnet` mapping.
201    pub state: Option<OvnBridgeMappingState>,
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub bridge: Option<String>,
204}
205
206// For Ord
207impl PartialOrd for OvnBridgeMapping {
208    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
209        Some(self.cmp(other))
210    }
211}
212
213// For Vec::sort_unstable()
214impl 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            // We want absent mapping listed before others
254            !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}