nmstate/ifaces/
hsr.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    BaseInterface, ErrorKind, Interface, InterfaceType, MergedInterfaces,
7    NmstateError,
8};
9
10#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
11#[serde(rename_all = "kebab-case")]
12#[non_exhaustive]
13/// HSR interface. The example YAML output of a
14/// [crate::NetworkState] with an HSR interface would be:
15/// ```yaml
16/// ---
17/// interfaces:
18///   - name: hsr0
19///     type: hsr
20///     state: up
21///     hsr:
22///       port1: eth1
23///       port2: eth2
24///       multicast-spec: 40
25///       protocol: prp
26/// ```
27///
28/// nmstate will configure the MAC addresses of HSR ports
29/// to be matching, to retain failover ability. It will
30/// use the MAC address of port1 by default, and apply it
31/// to port2 and the HSR interface. The MAC address can
32/// be overridden by setting the `mac-address` property
33/// on the HSR interface itself.
34pub struct HsrInterface {
35    #[serde(flatten)]
36    pub base: BaseInterface,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    /// Deserialize and serialize to `hsr`.
39    pub hsr: Option<HsrConfig>,
40}
41
42impl Default for HsrInterface {
43    fn default() -> Self {
44        Self {
45            base: BaseInterface {
46                iface_type: InterfaceType::Hsr,
47                ..Default::default()
48            },
49            hsr: None,
50        }
51    }
52}
53
54impl HsrInterface {
55    pub fn new() -> Self {
56        Self::default()
57    }
58
59    pub(crate) fn sanitize(
60        &mut self,
61        is_desired: bool,
62    ) -> Result<(), NmstateError> {
63        if is_desired {
64            if let Some(conf) = &mut self.hsr {
65                if let Some(address) = &mut conf.supervision_address {
66                    address.as_mut().make_ascii_uppercase();
67                    log::warn!(
68                        "The supervision-address is read-only, ignoring it on \
69                         desired state."
70                    );
71                }
72            }
73        }
74        Ok(())
75    }
76}
77
78impl MergedInterfaces {
79    pub(crate) fn get_hsr_mac(&self, iface: &HsrInterface) -> Option<String> {
80        if iface.base.mac_address.is_some() {
81            return iface.base.mac_address.clone();
82        }
83
84        let hsr_conf = iface.hsr.as_ref()?;
85
86        for use_permanent in [false, true] {
87            for port in [&hsr_conf.port1, &hsr_conf.port2] {
88                let mac = self
89                    .get_iface(port, InterfaceType::Unknown)
90                    .and_then(|iface| {
91                        let base = iface.merged.base_iface();
92                        if use_permanent {
93                            base.permanent_mac_address.clone()
94                        } else {
95                            base.mac_address.clone()
96                        }
97                    });
98                if mac.is_some() {
99                    return mac;
100                }
101            }
102        }
103
104        None
105    }
106
107    pub(crate) fn validate_hsr_mac(&self) -> Result<(), NmstateError> {
108        for hsr_iface in self.kernel_ifaces.iter().filter_map(|(_, iface)| {
109            if let Interface::Hsr(hsr_iface) = iface.desired.as_ref()? {
110                let hsr_conf = hsr_iface.hsr.as_ref()?;
111
112                if hsr_conf.protocol == HsrProtocol::Prp {
113                    return Some(hsr_iface);
114                }
115            }
116
117            None
118        }) {
119            let macs = [
120                hsr_iface.base.mac_address.as_deref(),
121                hsr_iface
122                    .hsr
123                    .as_ref()
124                    .and_then(|hsr_conf| {
125                        self.get_iface(&hsr_conf.port1, InterfaceType::Unknown)
126                    })
127                    .and_then(|iface| iface.desired.as_ref())
128                    .and_then(|iface| {
129                        iface.base_iface().mac_address.as_deref()
130                    }),
131                hsr_iface
132                    .hsr
133                    .as_ref()
134                    .and_then(|hsr_conf| {
135                        self.get_iface(&hsr_conf.port2, InterfaceType::Unknown)
136                    })
137                    .and_then(|iface| iface.desired.as_ref())
138                    .and_then(|iface| {
139                        iface.base_iface().mac_address.as_deref()
140                    }),
141            ];
142
143            let mut acc = None;
144
145            for &mac in macs.iter().flatten() {
146                if acc.is_some() && Some(mac) != acc {
147                    return Err(NmstateError::new(
148                        ErrorKind::InvalidArgument,
149                        format!(
150                            "HSR ports on interface {} cannot have different \
151                             MAC addresses",
152                            hsr_iface.base.name
153                        ),
154                    ));
155                }
156
157                acc = Some(mac);
158            }
159        }
160
161        Ok(())
162    }
163
164    pub(crate) fn copy_hsr_mac(&mut self) -> Result<(), NmstateError> {
165        let mut pending_changes = Vec::new();
166
167        for (hsr_iface, hsr_conf, mac) in
168            self.kernel_ifaces.iter().filter_map(|(_, iface)| {
169                if !iface.is_desired() {
170                    return None;
171                }
172
173                if let Interface::Hsr(hsr_iface) = &iface.merged {
174                    let mac = self.get_hsr_mac(hsr_iface)?;
175                    let hsr_conf = hsr_iface.hsr.as_ref()?;
176
177                    if hsr_conf.protocol != HsrProtocol::Prp {
178                        return None;
179                    }
180
181                    if iface.current.is_some() {
182                        if Some(mac.as_str())
183                            != self.kernel_ifaces.get(&hsr_conf.port2).and_then(
184                                |iface| {
185                                    iface
186                                        .merged
187                                        .base_iface()
188                                        .mac_address
189                                        .as_deref()
190                                },
191                            )
192                        {
193                            log::warn!(
194                                "Existing HSR PRP interface {} has \
195                                 mismatching MAC addresses on port1 and \
196                                 port2, which may break functionality",
197                                iface.merged.name()
198                            );
199                        }
200
201                        return None;
202                    }
203
204                    return Some((hsr_iface, hsr_conf, mac));
205                }
206
207                None
208            })
209        {
210            for ifname in
211                [&hsr_iface.base.name, &hsr_conf.port1, &hsr_conf.port2]
212            {
213                pending_changes.push((ifname.clone(), mac.clone()));
214            }
215        }
216
217        for (ifname, mac) in pending_changes {
218            if let Some(iface) = self.kernel_ifaces.get_mut(&ifname) {
219                iface.mark_as_changed();
220                iface.set_copy_from_mac(mac.clone());
221            }
222        }
223
224        Ok(())
225    }
226}
227
228#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
229#[serde(rename_all = "kebab-case")]
230#[non_exhaustive]
231#[derive(Default)]
232pub struct HsrConfig {
233    /// The port1 interface name.
234    pub port1: String,
235    /// The port2 interface name.
236    pub port2: String,
237    #[serde(skip_serializing_if = "Option::is_none")]
238    /// The MAC address used for the supervision frames. This property is
239    /// read-only.
240    pub supervision_address: Option<String>,
241    /// The last byte of the supervision address.
242    pub multicast_spec: u8,
243    /// Protocol to be used.
244    pub protocol: HsrProtocol,
245}
246
247impl HsrConfig {
248    pub fn new() -> Self {
249        Self::default()
250    }
251}
252
253#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
254#[serde(rename_all = "kebab-case")]
255#[non_exhaustive]
256#[derive(Default)]
257pub enum HsrProtocol {
258    #[default]
259    Hsr,
260    Prp,
261}