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;
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("static".to_string());
30///     iface.options.push(("address".to_string(), "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
186        // Check if the interface exists
187        let interface = self.interfaces.get(&vni_name)?;
188
189        // Look for the `bridge-access` option
190        for (key, value) in &interface.options {
191            if key == "bridge-access" {
192                // Try to parse the value as a u16
193                if let Ok(vlan_id) = value.parse::<u16>() {
194                    return Some(vlan_id);
195                }
196            }
197        }
198
199        None // No `bridge-access` option or invalid value
200    }
201
202    /// Retrieves all port names that have a `bridge-access` option defined.
203    ///
204    /// # Returns
205    ///
206    /// A `Vec<String>` containing the names of the ports with `bridge-access` defined.
207    pub fn get_bridge_interfaces(&self) -> Vec<String> {
208        self.interfaces
209            .iter()
210            .filter_map(|(name, iface)| {
211                for (key, _) in &iface.options {
212                    if key == "bridge-access" {
213                        return Some(name.clone());
214                    }
215                }
216                None
217            })
218            .collect()
219    }
220
221    /// Saves changes back to the `interfaces(5)` file.
222    ///
223    /// # Errors
224    ///
225    /// Returns a `NetworkInterfacesError` if the file cannot be written or has been modified on disk.
226    pub fn save(&mut self) -> Result<(), NetworkInterfacesError> {
227        let path = match &self.path {
228            Some(p) => p.clone(),
229            None => {
230                return Err(NetworkInterfacesError::Other(
231                    "No file path specified".to_string(),
232                ))
233            }
234        };
235
236        // Check if file has been modified since last load
237        let metadata = fs::metadata(&path)?;
238        let current_modified = metadata.modified()?;
239        if let Some(last_modified) = self.last_modified {
240            if current_modified > last_modified {
241                // File has been modified since last read
242                return Err(NetworkInterfacesError::FileModified);
243            }
244        }
245
246        // Write to the file using Display implementation
247        let mut file = fs::File::create(&path)?;
248        write!(file, "{}", self)?;
249
250        // Update last_modified
251        self.last_modified = Some(SystemTime::now());
252        Ok(())
253    }
254
255    /// Reloads the interfaces file from disk.
256    ///
257    /// # Errors
258    ///
259    /// Returns a `NetworkInterfacesError` if the file cannot be read or parsed.
260    pub fn reload(&mut self) -> Result<(), NetworkInterfacesError> {
261        let path = match &self.path {
262            Some(p) => p.clone(),
263            None => {
264                return Err(NetworkInterfacesError::Other(
265                    "No file path specified".to_string(),
266                ))
267            }
268        };
269        let reloaded = NetworkInterfaces::load(path)?;
270        self.interfaces = reloaded.interfaces;
271        self.comments = reloaded.comments;
272        self.sources = reloaded.sources;
273        self.last_modified = reloaded.last_modified;
274        Ok(())
275    }
276}
277
278// Implement Display for NetworkInterfaces to allow easy printing
279impl fmt::Display for NetworkInterfaces {
280    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281        // Print comments at the top if any
282        for comment in &self.comments {
283            writeln!(f, "{}", comment)?;
284        }
285
286        // Print source directives if any
287        for source in &self.sources {
288            writeln!(f, "{}", source)?;
289        }
290
291        // Collect interfaces into a vector and sort them by name
292        let mut interfaces: Vec<&Interface> = self.interfaces.values().collect();
293        interfaces.sort_by(|a, b| natural(&a.name, &b.name));
294
295        // Print interfaces
296        for iface in interfaces {
297            writeln!(f)?;
298            write!(f, "{}", iface)?;
299        }
300        Ok(())
301    }
302}
303
304// Implement methods to access interfaces directly if needed
305impl NetworkInterfaces {
306    /// Returns an iterator over the interfaces.
307    pub fn iter(&self) -> impl Iterator<Item = (&String, &Interface)> {
308        self.interfaces.iter()
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315
316    #[test]
317    fn test_next_unused_vlan_in_range() {
318        // Create a `NetworkInterfaces` instance with some used VLANs
319        let mut network_interfaces = NetworkInterfaces {
320            interfaces: HashMap::new(),
321            path: None,
322            last_modified: None,
323            comments: Vec::new(),
324            sources: Vec::new(),
325        };
326
327        // Add some VLAN interfaces to simulate used IDs
328        network_interfaces.add_interface(Interface::builder("vlan1000").build());
329        network_interfaces.add_interface(Interface::builder("vlan1001").build());
330        network_interfaces.add_interface(Interface::builder("vlan1003").build());
331
332        // Test: Find the next unused VLAN ID in the range 1000 to 1999
333        let next_vlan_id = network_interfaces.next_unused_vlan_in_range(1000, 1999);
334        assert_eq!(next_vlan_id, Some(1002));
335
336        // Test: Find the next unused VLAN ID when all VLANs are used in the range
337        network_interfaces.add_interface(Interface::builder("vlan1002").build());
338        let next_vlan_id = network_interfaces.next_unused_vlan_in_range(1000, 1003);
339        assert_eq!(next_vlan_id, None);
340
341        // Test: Find the next unused VLAN ID in a different range
342        let next_vlan_id = network_interfaces.next_unused_vlan_in_range(2000, 2005);
343        assert_eq!(next_vlan_id, Some(2000));
344    }
345
346    #[test]
347    fn test_get_existing_vni_vlan() {
348        let mut network_interfaces = NetworkInterfaces {
349            interfaces: HashMap::new(),
350            path: None,
351            last_modified: None,
352            comments: Vec::new(),
353            sources: Vec::new(),
354        };
355
356        // Add a VNI interface
357        network_interfaces.add_interface(
358            Interface::builder("vni123456")
359                .with_auto(true)
360                .with_option("bridge-access", "1002")
361                .build(),
362        );
363
364        // Test: Find VLAN ID for existing VNI
365        assert_eq!(network_interfaces.get_existing_vni_vlan(123456), Some(1002));
366
367        // Test: No `bridge-access` option
368        network_interfaces.add_interface(Interface::builder("vni987654").with_auto(true).build());
369        assert_eq!(network_interfaces.get_existing_vni_vlan(987654), None);
370
371        // Test: Nonexistent VNI
372        assert_eq!(network_interfaces.get_existing_vni_vlan(666), None);
373    }
374
375    #[test]
376    fn test_get_bridge_interfaces() {
377        let mut network_interfaces = NetworkInterfaces {
378            interfaces: HashMap::new(),
379            path: None,
380            last_modified: None,
381            comments: Vec::new(),
382            sources: Vec::new(),
383        };
384
385        // Add interfaces with `bridge-access`
386        network_interfaces.add_interface(
387            Interface::builder("vni1234")
388                .with_option("bridge-access", "1000")
389                .build(),
390        );
391        network_interfaces.add_interface(
392            Interface::builder("swp2")
393                .with_option("bridge-access", "1001")
394                .build(),
395        );
396
397        // Add an interface without `bridge-access`
398        network_interfaces.add_interface(Interface::builder("swp1").build());
399
400        // Verify the result
401        let bridge_interfaces = network_interfaces.get_bridge_interfaces();
402        assert_eq!(bridge_interfaces, vec!["swp2", "vni1234"]);
403    }
404}