interface_rs/interface/
option.rs

1//! Strongly typed interface options for `interfaces(5)` configuration.
2//!
3//! This module provides the [`InterfaceOption`] enum which represents all known
4//! interface configuration options with proper typing.
5
6use std::fmt;
7
8/// Represents a network interface configuration option with proper typing.
9///
10/// This enum provides strongly typed variants for common interface options,
11/// ensuring type safety and validation at compile time where possible.
12///
13/// Unknown options are captured in the [`InterfaceOption::Other`] variant.
14#[derive(Debug, Clone, PartialEq)]
15pub enum InterfaceOption {
16    /// IP address, optionally with CIDR notation (e.g., "192.168.1.100" or "192.168.1.100/24")
17    Address(String),
18    /// Network mask (e.g., "255.255.255.0")
19    Netmask(String),
20    /// Default gateway address
21    Gateway(String),
22    /// Broadcast address
23    Broadcast(String),
24    /// Network address
25    Network(String),
26    /// Maximum Transmission Unit
27    Mtu(u16),
28    /// VLAN ID for bridge access port
29    BridgeAccess(u16),
30    /// List of bridge member ports
31    BridgePorts(Vec<String>),
32    /// Bridge Port VLAN ID (PVID)
33    BridgePvid(u16),
34    /// Bridge VLAN IDs (can be ranges like "100-154 199")
35    BridgeVids(String),
36    /// Whether the bridge is VLAN-aware
37    BridgeVlanAware(bool),
38    /// MSTP BPDU guard setting
39    MstpctlBpduguard(bool),
40    /// MSTP port admin edge setting
41    MstpctlPortadminedge(bool),
42    /// Script to run after interface comes up
43    PostUp(String),
44    /// Script to run before interface goes down
45    PreDown(String),
46    /// Script to run after interface goes down
47    PostDown(String),
48    /// Script to run before interface comes up
49    PreUp(String),
50    /// VRF name
51    Vrf(String),
52    /// VRF table (can be "auto" or a number)
53    VrfTable(String),
54    /// VLAN ID
55    VlanId(u16),
56    /// Raw device for VLAN
57    VlanRawDevice(String),
58    /// Hardware address (MAC)
59    HwAddress(String),
60    /// DNS nameservers
61    DnsNameservers(String),
62    /// DNS search domains
63    DnsSearch(String),
64    /// Metric for the route
65    Metric(u32),
66    /// Point-to-point address
67    Pointopoint(String),
68    /// Media type
69    Media(String),
70    /// Any other option not explicitly defined
71    Other(String, String),
72}
73
74impl InterfaceOption {
75    /// Returns the option name as it appears in the interfaces file.
76    pub fn name(&self) -> &str {
77        match self {
78            InterfaceOption::Address(_) => "address",
79            InterfaceOption::Netmask(_) => "netmask",
80            InterfaceOption::Gateway(_) => "gateway",
81            InterfaceOption::Broadcast(_) => "broadcast",
82            InterfaceOption::Network(_) => "network",
83            InterfaceOption::Mtu(_) => "mtu",
84            InterfaceOption::BridgeAccess(_) => "bridge-access",
85            InterfaceOption::BridgePorts(_) => "bridge-ports",
86            InterfaceOption::BridgePvid(_) => "bridge-pvid",
87            InterfaceOption::BridgeVids(_) => "bridge-vids",
88            InterfaceOption::BridgeVlanAware(_) => "bridge-vlan-aware",
89            InterfaceOption::MstpctlBpduguard(_) => "mstpctl-bpduguard",
90            InterfaceOption::MstpctlPortadminedge(_) => "mstpctl-portadminedge",
91            InterfaceOption::PostUp(_) => "post-up",
92            InterfaceOption::PreDown(_) => "pre-down",
93            InterfaceOption::PostDown(_) => "post-down",
94            InterfaceOption::PreUp(_) => "pre-up",
95            InterfaceOption::Vrf(_) => "vrf",
96            InterfaceOption::VrfTable(_) => "vrf-table",
97            InterfaceOption::VlanId(_) => "vlan-id",
98            InterfaceOption::VlanRawDevice(_) => "vlan-raw-device",
99            InterfaceOption::HwAddress(_) => "hwaddress",
100            InterfaceOption::DnsNameservers(_) => "dns-nameservers",
101            InterfaceOption::DnsSearch(_) => "dns-search",
102            InterfaceOption::Metric(_) => "metric",
103            InterfaceOption::Pointopoint(_) => "pointopoint",
104            InterfaceOption::Media(_) => "media",
105            InterfaceOption::Other(name, _) => name,
106        }
107    }
108
109    /// Returns the option value as a string.
110    pub fn value(&self) -> String {
111        match self {
112            InterfaceOption::Address(v) => v.clone(),
113            InterfaceOption::Netmask(v) => v.clone(),
114            InterfaceOption::Gateway(v) => v.clone(),
115            InterfaceOption::Broadcast(v) => v.clone(),
116            InterfaceOption::Network(v) => v.clone(),
117            InterfaceOption::Mtu(v) => v.to_string(),
118            InterfaceOption::BridgeAccess(v) => v.to_string(),
119            InterfaceOption::BridgePorts(v) => v.join(" "),
120            InterfaceOption::BridgePvid(v) => v.to_string(),
121            InterfaceOption::BridgeVids(v) => v.clone(),
122            InterfaceOption::BridgeVlanAware(v) => if *v { "yes" } else { "no" }.to_string(),
123            InterfaceOption::MstpctlBpduguard(v) => if *v { "yes" } else { "no" }.to_string(),
124            InterfaceOption::MstpctlPortadminedge(v) => if *v { "yes" } else { "no" }.to_string(),
125            InterfaceOption::PostUp(v) => v.clone(),
126            InterfaceOption::PreDown(v) => v.clone(),
127            InterfaceOption::PostDown(v) => v.clone(),
128            InterfaceOption::PreUp(v) => v.clone(),
129            InterfaceOption::Vrf(v) => v.clone(),
130            InterfaceOption::VrfTable(v) => v.clone(),
131            InterfaceOption::VlanId(v) => v.to_string(),
132            InterfaceOption::VlanRawDevice(v) => v.clone(),
133            InterfaceOption::HwAddress(v) => v.clone(),
134            InterfaceOption::DnsNameservers(v) => v.clone(),
135            InterfaceOption::DnsSearch(v) => v.clone(),
136            InterfaceOption::Metric(v) => v.to_string(),
137            InterfaceOption::Pointopoint(v) => v.clone(),
138            InterfaceOption::Media(v) => v.clone(),
139            InterfaceOption::Other(_, v) => v.clone(),
140        }
141    }
142
143    /// Creates an InterfaceOption from a key-value pair.
144    ///
145    /// This parses the value into the appropriate type based on the key.
146    pub fn from_key_value(key: &str, value: &str) -> Self {
147        match key {
148            "address" => InterfaceOption::Address(value.to_string()),
149            "netmask" => InterfaceOption::Netmask(value.to_string()),
150            "gateway" => InterfaceOption::Gateway(value.to_string()),
151            "broadcast" => InterfaceOption::Broadcast(value.to_string()),
152            "network" => InterfaceOption::Network(value.to_string()),
153            "mtu" => value
154                .parse()
155                .map(InterfaceOption::Mtu)
156                .unwrap_or_else(|_| InterfaceOption::Other(key.to_string(), value.to_string())),
157            "bridge-access" => value
158                .parse()
159                .map(InterfaceOption::BridgeAccess)
160                .unwrap_or_else(|_| InterfaceOption::Other(key.to_string(), value.to_string())),
161            "bridge-ports" => {
162                InterfaceOption::BridgePorts(value.split_whitespace().map(String::from).collect())
163            }
164            "bridge-pvid" => value
165                .parse()
166                .map(InterfaceOption::BridgePvid)
167                .unwrap_or_else(|_| InterfaceOption::Other(key.to_string(), value.to_string())),
168            "bridge-vids" => InterfaceOption::BridgeVids(value.to_string()),
169            "bridge-vlan-aware" => InterfaceOption::BridgeVlanAware(parse_bool(value)),
170            "mstpctl-bpduguard" => InterfaceOption::MstpctlBpduguard(parse_bool(value)),
171            "mstpctl-portadminedge" => InterfaceOption::MstpctlPortadminedge(parse_bool(value)),
172            "post-up" => InterfaceOption::PostUp(value.to_string()),
173            "pre-down" => InterfaceOption::PreDown(value.to_string()),
174            "post-down" => InterfaceOption::PostDown(value.to_string()),
175            "pre-up" => InterfaceOption::PreUp(value.to_string()),
176            "vrf" => InterfaceOption::Vrf(value.to_string()),
177            "vrf-table" => InterfaceOption::VrfTable(value.to_string()),
178            "vlan-id" => value
179                .parse()
180                .map(InterfaceOption::VlanId)
181                .unwrap_or_else(|_| InterfaceOption::Other(key.to_string(), value.to_string())),
182            "vlan-raw-device" => InterfaceOption::VlanRawDevice(value.to_string()),
183            "hwaddress" => InterfaceOption::HwAddress(value.to_string()),
184            "dns-nameservers" => InterfaceOption::DnsNameservers(value.to_string()),
185            "dns-search" => InterfaceOption::DnsSearch(value.to_string()),
186            "metric" => value
187                .parse()
188                .map(InterfaceOption::Metric)
189                .unwrap_or_else(|_| InterfaceOption::Other(key.to_string(), value.to_string())),
190            "pointopoint" => InterfaceOption::Pointopoint(value.to_string()),
191            "media" => InterfaceOption::Media(value.to_string()),
192            _ => InterfaceOption::Other(key.to_string(), value.to_string()),
193        }
194    }
195
196    /// Converts the option back to a key-value tuple.
197    pub fn to_key_value(&self) -> (String, String) {
198        (self.name().to_string(), self.value())
199    }
200}
201
202impl fmt::Display for InterfaceOption {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        write!(f, "{} {}", self.name(), self.value())
205    }
206}
207
208/// Helper function to parse boolean values from interface files.
209fn parse_bool(value: &str) -> bool {
210    matches!(value.to_lowercase().as_str(), "yes" | "on" | "true" | "1")
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn test_from_key_value_address() {
219        let opt = InterfaceOption::from_key_value("address", "192.168.1.100");
220        assert_eq!(opt, InterfaceOption::Address("192.168.1.100".to_string()));
221        assert_eq!(opt.name(), "address");
222        assert_eq!(opt.value(), "192.168.1.100");
223    }
224
225    #[test]
226    fn test_from_key_value_mtu() {
227        let opt = InterfaceOption::from_key_value("mtu", "9216");
228        assert_eq!(opt, InterfaceOption::Mtu(9216));
229        assert_eq!(opt.name(), "mtu");
230        assert_eq!(opt.value(), "9216");
231    }
232
233    #[test]
234    fn test_from_key_value_bridge_ports() {
235        let opt = InterfaceOption::from_key_value("bridge-ports", "swp1 swp2 swp3");
236        assert_eq!(
237            opt,
238            InterfaceOption::BridgePorts(vec![
239                "swp1".to_string(),
240                "swp2".to_string(),
241                "swp3".to_string()
242            ])
243        );
244        assert_eq!(opt.value(), "swp1 swp2 swp3");
245    }
246
247    #[test]
248    fn test_from_key_value_bridge_vlan_aware() {
249        let opt_yes = InterfaceOption::from_key_value("bridge-vlan-aware", "yes");
250        assert_eq!(opt_yes, InterfaceOption::BridgeVlanAware(true));
251        assert_eq!(opt_yes.value(), "yes");
252
253        let opt_no = InterfaceOption::from_key_value("bridge-vlan-aware", "no");
254        assert_eq!(opt_no, InterfaceOption::BridgeVlanAware(false));
255        assert_eq!(opt_no.value(), "no");
256    }
257
258    #[test]
259    fn test_from_key_value_unknown() {
260        let opt = InterfaceOption::from_key_value("custom-option", "custom-value");
261        assert_eq!(
262            opt,
263            InterfaceOption::Other("custom-option".to_string(), "custom-value".to_string())
264        );
265        assert_eq!(opt.name(), "custom-option");
266        assert_eq!(opt.value(), "custom-value");
267    }
268
269    #[test]
270    fn test_display() {
271        let opt = InterfaceOption::Mtu(1500);
272        assert_eq!(format!("{}", opt), "mtu 1500");
273
274        let opt = InterfaceOption::BridgeVlanAware(true);
275        assert_eq!(format!("{}", opt), "bridge-vlan-aware yes");
276    }
277
278    #[test]
279    fn test_to_key_value() {
280        let opt = InterfaceOption::Gateway("192.168.1.1".to_string());
281        let (key, value) = opt.to_key_value();
282        assert_eq!(key, "gateway");
283        assert_eq!(value, "192.168.1.1");
284    }
285}