interface_rs/
network_interfaces.rs

1use crate::error::NetworkInterfacesError;
2use crate::interface::Interface;
3use crate::parser::Parser;
4use crate::helper::sort::natural;
5use std::collections::HashMap;
6use std::fmt;
7use std::fs;
8use std::io::Write;
9use std::path::{Path, PathBuf};
10use std::time::SystemTime;
11
12/// Represents the collection of network interfaces defined in an `interfaces(5)` file.
13///
14/// The `NetworkInterfaces` struct provides methods to load, manipulate, and save
15/// network interface configurations.
16///
17/// # Examples
18///
19/// Loading and modifying interfaces:
20///
21/// ```rust
22/// use interface_rs::NetworkInterfaces;
23/// use interface_rs::interface::{Interface, InterfaceOption, Method};
24///
25/// let mut net_ifaces = NetworkInterfaces::load("tests/interfaces").unwrap();
26///
27/// // Modify an interface
28/// if let Some(iface) = net_ifaces.get_interface_mut("eth0") {
29///     iface.method = Some(Method::Static);
30///     iface.options.push(InterfaceOption::Address("192.168.1.100".to_string()));
31/// }
32///
33/// // Save changes
34/// net_ifaces.save().unwrap();
35/// ```
36#[derive(Debug)]
37pub struct NetworkInterfaces {
38    /// A mapping of interface names to their configurations.
39    interfaces: HashMap<String, Interface>,
40    /// The path to the interfaces file.
41    path: Option<PathBuf>,
42    /// The last modified time of the interfaces file.
43    last_modified: Option<SystemTime>,
44    /// Comments from the original file
45    comments: Vec<String>,
46    /// Source directives from the original file
47    sources: Vec<String>,
48}
49
50impl NetworkInterfaces {
51    /// Creates a new `NetworkInterfaces` instance.
52    fn new(
53        interfaces: HashMap<String, Interface>,
54        comments: Vec<String>,
55        sources: Vec<String>,
56        path: Option<PathBuf>,
57        last_modified: Option<SystemTime>,
58    ) -> Self {
59        NetworkInterfaces {
60            interfaces,
61            comments,
62            sources,
63            path,
64            last_modified,
65        }
66    }
67
68    /// Loads the `interfaces(5)` file into memory.
69    ///
70    /// # Arguments
71    ///
72    /// * `path` - The path to the interfaces file.
73    ///
74    /// # Returns
75    ///
76    /// A `NetworkInterfaces` instance containing the parsed interfaces.
77    ///
78    /// # Errors
79    ///
80    /// Returns a `NetworkInterfacesError` if the file cannot be read or parsed.
81    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, NetworkInterfacesError> {
82        let path_buf = path.as_ref().to_path_buf();
83        let metadata = fs::metadata(&path_buf)?;
84        let last_modified = metadata.modified()?;
85
86        let content = fs::read_to_string(&path_buf)?;
87        let parser = Parser::new();
88        let (interfaces, comments, sources) = parser.parse(&content)?;
89
90        Ok(NetworkInterfaces::new(
91            interfaces,
92            comments,
93            sources,
94            Some(path_buf),
95            Some(last_modified),
96        ))
97    }
98
99    /// Retrieves a reference to an interface by name.
100    ///
101    /// # Arguments
102    ///
103    /// * `name` - The name of the interface.
104    ///
105    /// # Returns
106    ///
107    /// An `Option` containing a reference to the `Interface` if found.
108    pub fn get_interface(&self, name: &str) -> Option<&Interface> {
109        self.interfaces.get(name)
110    }
111
112    /// Retrieves a mutable reference to an interface by name.
113    ///
114    /// # Arguments
115    ///
116    /// * `name` - The name of the interface.
117    ///
118    /// # Returns
119    ///
120    /// An `Option` containing a mutable reference to the `Interface` if found.
121    pub fn get_interface_mut(&mut self, name: &str) -> Option<&mut Interface> {
122        self.interfaces.get_mut(name)
123    }
124
125    /// Adds or updates an interface in the collection.
126    ///
127    /// # Arguments
128    ///
129    /// * `iface` - The `Interface` to add or update.
130    pub fn add_interface(&mut self, iface: Interface) {
131        self.interfaces.insert(iface.name.clone(), iface);
132    }
133
134    /// Deletes an interface by name.
135    ///
136    /// # Arguments
137    ///
138    /// * `name` - The name of the interface to delete.
139    pub fn delete_interface(&mut self, name: &str) {
140        self.interfaces.remove(name);
141    }
142
143    /// Returns the number of interfaces.
144    pub fn len(&self) -> usize {
145        self.interfaces.len()
146    }
147
148    /// Checks if the collection is empty.
149    pub fn is_empty(&self) -> bool {
150        self.interfaces.is_empty()
151    }
152
153    /// Finds the next unused VLAN ID within a specified range.
154    ///
155    /// # Arguments
156    ///
157    /// * `start` - The starting VLAN ID (inclusive).
158    /// * `end` - The ending VLAN ID (inclusive).
159    ///
160    /// # Returns
161    ///
162    /// * `Option<u16>` - The next unused VLAN ID, or `None` if all are used.
163    pub fn next_unused_vlan_in_range(&self, start: u16, end: u16) -> Option<u16> {
164        for vlan_id in start..=end {
165            let vlan_name = format!("vlan{}", vlan_id);
166            if !self.interfaces.contains_key(&vlan_name) {
167                return Some(vlan_id);
168            }
169        }
170        None // All VLAN IDs in the specified range are used
171    }
172
173    /// Retrieves the VLAN associated with an existing VNI interface.
174    ///
175    /// # Arguments
176    ///
177    /// * `vni_id` - The VNI ID to search for (e.g., `1347682`).
178    ///
179    /// # Returns
180    ///
181    /// * `Option<u16>` - The VLAN ID specified in the `bridge-access` option, or `None` if
182    ///   the interface does not exist or the option is not present.
183    pub fn get_existing_vni_vlan(&self, vni_id: u32) -> Option<u16> {
184        let vni_name = format!("vni{}", vni_id);
185        let interface = self.interfaces.get(&vni_name)?;
186        interface.get_option("bridge-access").and_then(|v| v.parse().ok())
187    }
188
189    /// Retrieves all port names that have a `bridge-access` option defined.
190    ///
191    /// # Returns
192    ///
193    /// A `Vec<String>` containing the names of the ports with `bridge-access` defined.
194    pub fn get_bridge_interfaces(&self) -> Vec<String> {
195        self.interfaces
196            .iter()
197            .filter_map(|(name, iface)| {
198                iface.get_option("bridge-access").map(|_| name.clone())
199            })
200            .collect()
201    }
202
203    /// Saves changes back to the `interfaces(5)` file.
204    ///
205    /// # Errors
206    ///
207    /// Returns a `NetworkInterfacesError` if the file cannot be written or has been modified on disk.
208    pub fn save(&mut self) -> Result<(), NetworkInterfacesError> {
209        let path = match &self.path {
210            Some(p) => p.clone(),
211            None => {
212                return Err(NetworkInterfacesError::Other(
213                    "No file path specified".to_string(),
214                ))
215            }
216        };
217
218        // Check if file has been modified since last load
219        let metadata = fs::metadata(&path)?;
220        let current_modified = metadata.modified()?;
221        if let Some(last_modified) = self.last_modified {
222            if current_modified > last_modified {
223                // File has been modified since last read
224                return Err(NetworkInterfacesError::FileModified);
225            }
226        }
227
228        // Write to the file using Display implementation
229        let mut file = fs::File::create(&path)?;
230        write!(file, "{}", self)?;
231
232        // Update last_modified
233        self.last_modified = Some(SystemTime::now());
234        Ok(())
235    }
236
237    /// Reloads the interfaces file from disk.
238    ///
239    /// # Errors
240    ///
241    /// Returns a `NetworkInterfacesError` if the file cannot be read or parsed.
242    pub fn reload(&mut self) -> Result<(), NetworkInterfacesError> {
243        let path = match &self.path {
244            Some(p) => p.clone(),
245            None => {
246                return Err(NetworkInterfacesError::Other(
247                    "No file path specified".to_string(),
248                ))
249            }
250        };
251        let reloaded = NetworkInterfaces::load(path)?;
252        self.interfaces = reloaded.interfaces;
253        self.comments = reloaded.comments;
254        self.sources = reloaded.sources;
255        self.last_modified = reloaded.last_modified;
256        Ok(())
257    }
258}
259
260// Implement Display for NetworkInterfaces to allow easy printing
261impl fmt::Display for NetworkInterfaces {
262    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263        // Print comments at the top if any
264        for comment in &self.comments {
265            writeln!(f, "{}", comment)?;
266        }
267
268        // Print source directives if any
269        for source in &self.sources {
270            writeln!(f, "{}", source)?;
271        }
272
273        // Collect interfaces into a vector and sort them by name
274        let mut interfaces: Vec<&Interface> = self.interfaces.values().collect();
275        interfaces.sort_by(|a, b| natural(&a.name, &b.name));
276
277        // Print interfaces
278        for iface in interfaces {
279            writeln!(f)?;
280            write!(f, "{}", iface)?;
281        }
282        Ok(())
283    }
284}
285
286// Implement methods to access interfaces directly if needed
287impl NetworkInterfaces {
288    /// Returns an iterator over the interfaces.
289    pub fn iter(&self) -> impl Iterator<Item = (&String, &Interface)> {
290        self.interfaces.iter()
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297
298    #[test]
299    fn test_next_unused_vlan_in_range() {
300        // Create a `NetworkInterfaces` instance with some used VLANs
301        let mut network_interfaces = NetworkInterfaces {
302            interfaces: HashMap::new(),
303            path: None,
304            last_modified: None,
305            comments: Vec::new(),
306            sources: Vec::new(),
307        };
308
309        // Add some VLAN interfaces to simulate used IDs
310        network_interfaces.add_interface(Interface::builder("vlan1000").build());
311        network_interfaces.add_interface(Interface::builder("vlan1001").build());
312        network_interfaces.add_interface(Interface::builder("vlan1003").build());
313
314        // Test: Find the next unused VLAN ID in the range 1000 to 1999
315        let next_vlan_id = network_interfaces.next_unused_vlan_in_range(1000, 1999);
316        assert_eq!(next_vlan_id, Some(1002));
317
318        // Test: Find the next unused VLAN ID when all VLANs are used in the range
319        network_interfaces.add_interface(Interface::builder("vlan1002").build());
320        let next_vlan_id = network_interfaces.next_unused_vlan_in_range(1000, 1003);
321        assert_eq!(next_vlan_id, None);
322
323        // Test: Find the next unused VLAN ID in a different range
324        let next_vlan_id = network_interfaces.next_unused_vlan_in_range(2000, 2005);
325        assert_eq!(next_vlan_id, Some(2000));
326    }
327
328    #[test]
329    fn test_get_existing_vni_vlan() {
330        let mut network_interfaces = NetworkInterfaces {
331            interfaces: HashMap::new(),
332            path: None,
333            last_modified: None,
334            comments: Vec::new(),
335            sources: Vec::new(),
336        };
337
338        // Add a VNI interface
339        network_interfaces.add_interface(
340            Interface::builder("vni123456")
341                .with_auto(true)
342                .with_option("bridge-access", "1002")
343                .build(),
344        );
345
346        // Test: Find VLAN ID for existing VNI
347        assert_eq!(network_interfaces.get_existing_vni_vlan(123456), Some(1002));
348
349        // Test: No `bridge-access` option
350        network_interfaces.add_interface(Interface::builder("vni987654").with_auto(true).build());
351        assert_eq!(network_interfaces.get_existing_vni_vlan(987654), None);
352
353        // Test: Nonexistent VNI
354        assert_eq!(network_interfaces.get_existing_vni_vlan(666), None);
355    }
356
357    #[test]
358    fn test_get_bridge_interfaces() {
359        let mut network_interfaces = NetworkInterfaces {
360            interfaces: HashMap::new(),
361            path: None,
362            last_modified: None,
363            comments: Vec::new(),
364            sources: Vec::new(),
365        };
366
367        // Add interfaces with `bridge-access`
368        network_interfaces.add_interface(
369            Interface::builder("vni1234")
370                .with_option("bridge-access", "1000")
371                .build(),
372        );
373        network_interfaces.add_interface(
374            Interface::builder("swp2")
375                .with_option("bridge-access", "1001")
376                .build(),
377        );
378
379        // Add an interface without `bridge-access`
380        network_interfaces.add_interface(Interface::builder("swp1").build());
381
382        // Verify the result (sort for deterministic comparison)
383        let mut bridge_interfaces = network_interfaces.get_bridge_interfaces();
384        bridge_interfaces.sort();
385        assert_eq!(bridge_interfaces, vec!["swp2", "vni1234"]);
386    }
387}