netplan_types/netplan/
dhcp.rs

1#[cfg(feature = "serde")]
2use serde::{Deserialize, Serialize};
3
4#[cfg(feature = "derive_builder")]
5use derive_builder::Builder;
6
7/// Several DHCP behavior overrides are available. Most currently only have any
8/// effect when using the networkd backend, with the exception of use-routes
9/// and route-metric.
10///
11/// Overrides only have an effect if the corresponding dhcp4 or dhcp6 is
12/// set to true.
13///
14/// If both dhcp4 and dhcp6 are true, the networkd backend requires
15/// that dhcp4-overrides and dhcp6-overrides contain the same keys and
16/// values. If the values do not match, an error will be shown and the network
17/// configuration will not be applied.
18///
19/// When using the NetworkManager backend, different values may be specified for
20/// dhcp4-overrides and dhcp6-overrides, and will be applied to the DHCP
21/// client processes as specified in the netplan YAML.
22#[derive(Default, Debug, Clone, PartialEq, Eq)]
23#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
24#[cfg_attr(feature = "derive_builder", derive(Builder))]
25#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
26#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
27pub struct DhcpOverrides {
28    /// Default: true. When true, the DNS servers received from the
29    /// DHCP server will be used and take precedence over any statically
30    /// configured ones. Currently only has an effect on the networkd
31    /// backend.
32    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
33    #[cfg_attr(feature = "serde", serde(default))]
34    #[cfg_attr(
35        feature = "serde",
36        serde(deserialize_with = "crate::bool::string_or_bool_option")
37    )]
38    pub use_dns: Option<bool>,
39    /// Default: true. When true, the NTP servers received from the
40    /// DHCP server will be used by systemd-timesyncd and take precedence
41    /// over any statically configured ones. Currently only has an effect on
42    /// the networkd backend.
43    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
44    #[cfg_attr(feature = "serde", serde(default))]
45    #[cfg_attr(
46        feature = "serde",
47        serde(deserialize_with = "crate::bool::string_or_bool_option")
48    )]
49    pub use_ntp: Option<bool>,
50    /// Default: true. When true, the machine’s hostname will be sent
51    /// to the DHCP server. Currently only has an effect on the networkd
52    /// backend.
53    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
54    #[cfg_attr(feature = "serde", serde(default))]
55    #[cfg_attr(
56        feature = "serde",
57        serde(deserialize_with = "crate::bool::string_or_bool_option")
58    )]
59    pub send_hostname: Option<bool>,
60    /// Default: true. When true, the hostname received from the DHCP
61    /// server will be set as the transient hostname of the system. Currently
62    /// only has an effect on the networkd backend.
63    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
64    #[cfg_attr(feature = "serde", serde(default))]
65    #[cfg_attr(
66        feature = "serde",
67        serde(deserialize_with = "crate::bool::string_or_bool_option")
68    )]
69    pub use_hostname: Option<bool>,
70    /// Default: true. When true, the MTU received from the DHCP
71    /// server will be set as the MTU of the network interface. When false,
72    /// the MTU advertised by the DHCP server will be ignored. Currently only
73    /// has an effect on the networkd backend.
74    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
75    #[cfg_attr(feature = "serde", serde(default))]
76    #[cfg_attr(
77        feature = "serde",
78        serde(deserialize_with = "crate::bool::string_or_bool_option")
79    )]
80    pub use_mtu: Option<bool>,
81    /// Use this value for the hostname which is sent to the DHCP server,
82    /// instead of machine’s hostname. Currently only has an effect on the
83    /// networkd backend.
84    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
85    pub hostname: Option<String>,
86    /// Default: true. When true, the routes received from the DHCP
87    /// server will be installed in the routing table normally. When set to
88    /// false, routes from the DHCP server will be ignored: in this case,
89    /// the user is responsible for adding static routes if necessary for
90    /// correct network operation. This allows users to avoid installing a
91    /// default gateway for interfaces configured via DHCP. Available for
92    /// both the networkd and NetworkManager backends.
93    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
94    #[cfg_attr(feature = "serde", serde(default))]
95    #[cfg_attr(
96        feature = "serde",
97        serde(deserialize_with = "crate::bool::string_or_bool_option")
98    )]
99    pub use_routes: Option<bool>,
100    /// Use this value for default metric for automatically-added routes.
101    /// Use this to prioritize routes for devices by setting a lower metric
102    /// on a preferred interface. Available for both the networkd and
103    /// NetworkManager backends.
104    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
105    pub route_metric: Option<u16>,
106    /// Takes a boolean, or the special value “route”. When true, the domain
107    /// name received from the DHCP server will be used as DNS search domain
108    /// over this link, similar to the effect of the Domains= setting. If set
109    /// to “route”, the domain name received from the DHCP server will be
110    /// used for routing DNS queries only, but not for searching, similar to
111    /// the effect of the Domains= setting when the argument is prefixed with
112    /// “~”.
113    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
114    pub use_domains: Option<String>,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
118#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
119#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
120pub enum Ipv6AddressGeneration {
121    #[cfg_attr(feature = "serde", serde(rename = "eui64"))]
122    Eui64,
123    #[cfg_attr(feature = "serde", serde(rename = "stable-privacy"))]
124    StablePrivacy,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
128#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
129#[cfg_attr(feature = "serde", serde(untagged))]
130#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
131pub enum AddressMapping {
132    Simple(String),
133    Complex(std::collections::HashMap<String, AddressProperties>),
134}
135
136#[derive(Default, Debug, Clone, PartialEq, Eq)]
137#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
138#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
139#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
140pub struct AddressProperties {
141    /// An IP address label, equivalent to the ip address label
142    /// command. Currently supported on the networkd backend only.
143    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
144    pub label: Option<String>,
145
146    /// Default: forever. This can be forever or 0 and corresponds
147    /// to the PreferredLifetime option in systemd-networkd's Address
148    /// section. Currently supported on the networkd backend only.
149    /// Since 0.100.
150    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
151    pub lifetime: Option<PreferredLifetime>,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
155#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
156#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
157pub enum PreferredLifetime {
158    #[cfg_attr(feature = "serde", serde(rename = "forever"))]
159    Forever,
160    #[cfg_attr(feature = "serde", serde(rename = "0"))]
161    Zero,
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn test_dhcp_overrides_defaults() {
170        let overrides = DhcpOverrides::default();
171        assert_eq!(overrides.use_dns, None);
172        assert_eq!(overrides.use_ntp, None);
173        assert_eq!(overrides.send_hostname, None);
174        assert_eq!(overrides.use_hostname, None);
175        assert_eq!(overrides.use_mtu, None);
176        assert_eq!(overrides.hostname, None);
177        assert_eq!(overrides.use_routes, None);
178        assert_eq!(overrides.route_metric, None);
179        assert_eq!(overrides.use_domains, None);
180    }
181
182    #[test]
183    #[cfg(feature = "serde")]
184    fn test_dhcp_overrides_serialize() {
185        let overrides = DhcpOverrides {
186            use_dns: Some(false),
187            use_ntp: Some(true),
188            send_hostname: Some(true),
189            use_hostname: Some(false),
190            use_mtu: Some(true),
191            hostname: Some("test-host".to_string()),
192            use_routes: Some(false),
193            route_metric: Some(100),
194            use_domains: Some("route".to_string()),
195        };
196
197        let yaml = serde_yaml::to_string(&overrides).unwrap();
198        assert!(yaml.contains("use-dns: false"));
199        assert!(yaml.contains("use-ntp: true"));
200        assert!(yaml.contains("send-hostname: true"));
201        assert!(yaml.contains("use-hostname: false"));
202        assert!(yaml.contains("use-mtu: true"));
203        assert!(yaml.contains("hostname: test-host"));
204        assert!(yaml.contains("use-routes: false"));
205        assert!(yaml.contains("route-metric: 100"));
206        assert!(yaml.contains("use-domains: route"));
207    }
208
209    #[test]
210    #[cfg(feature = "serde")]
211    fn test_dhcp_overrides_deserialize() {
212        let yaml = r#"
213use-dns: false
214use-ntp: true
215send-hostname: true
216use-hostname: false
217use-mtu: true
218hostname: test-host
219use-routes: false
220route-metric: 100
221use-domains: route
222"#;
223
224        let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
225        assert_eq!(overrides.use_dns, Some(false));
226        assert_eq!(overrides.use_ntp, Some(true));
227        assert_eq!(overrides.send_hostname, Some(true));
228        assert_eq!(overrides.use_hostname, Some(false));
229        assert_eq!(overrides.use_mtu, Some(true));
230        assert_eq!(overrides.hostname, Some("test-host".to_string()));
231        assert_eq!(overrides.use_routes, Some(false));
232        assert_eq!(overrides.route_metric, Some(100));
233        assert_eq!(overrides.use_domains, Some("route".to_string()));
234    }
235
236    #[test]
237    #[cfg(feature = "serde")]
238    fn test_dhcp_overrides_skip_none_serialization() {
239        let overrides = DhcpOverrides {
240            use_dns: Some(false),
241            use_ntp: None,
242            ..Default::default()
243        };
244
245        let yaml = serde_yaml::to_string(&overrides).unwrap();
246        assert!(yaml.contains("use-dns: false"));
247        assert!(!yaml.contains("use-ntp"));
248        assert!(!yaml.contains("send-hostname"));
249    }
250
251    #[test]
252    #[cfg(feature = "serde")]
253    fn test_dhcp_overrides_bool_as_string() {
254        let yaml = r#"
255use-dns: "false"
256use-ntp: "true"
257"#;
258
259        let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
260        assert_eq!(overrides.use_dns, Some(false));
261        assert_eq!(overrides.use_ntp, Some(true));
262    }
263
264    #[test]
265    #[cfg(feature = "serde")]
266    fn test_ipv6_address_generation_serialize() {
267        let eui64 = Ipv6AddressGeneration::Eui64;
268        let stable = Ipv6AddressGeneration::StablePrivacy;
269
270        let eui64_yaml = serde_yaml::to_string(&eui64).unwrap();
271        let stable_yaml = serde_yaml::to_string(&stable).unwrap();
272
273        assert!(eui64_yaml.contains("eui64"));
274        assert!(stable_yaml.contains("stable-privacy"));
275    }
276
277    #[test]
278    #[cfg(feature = "serde")]
279    fn test_ipv6_address_generation_deserialize() {
280        let eui64: Ipv6AddressGeneration = serde_yaml::from_str("eui64").unwrap();
281        let stable: Ipv6AddressGeneration = serde_yaml::from_str("stable-privacy").unwrap();
282
283        assert_eq!(eui64, Ipv6AddressGeneration::Eui64);
284        assert_eq!(stable, Ipv6AddressGeneration::StablePrivacy);
285    }
286
287    #[test]
288    #[cfg(feature = "serde")]
289    fn test_address_mapping_simple() {
290        let simple = AddressMapping::Simple("192.168.1.10/24".to_string());
291
292        let yaml = serde_yaml::to_string(&simple).unwrap();
293        assert_eq!(yaml.trim(), "192.168.1.10/24");
294
295        let deserialized: AddressMapping = serde_yaml::from_str("192.168.1.10/24").unwrap();
296        assert_eq!(deserialized, simple);
297    }
298
299    #[test]
300    #[cfg(feature = "serde")]
301    fn test_address_mapping_complex() {
302        let mut map = std::collections::HashMap::new();
303        map.insert(
304            "192.168.1.10/24".to_string(),
305            AddressProperties {
306                label: Some("my-label".to_string()),
307                lifetime: Some(PreferredLifetime::Forever),
308            },
309        );
310        let complex = AddressMapping::Complex(map);
311
312        let yaml = serde_yaml::to_string(&complex).unwrap();
313        assert!(yaml.contains("192.168.1.10/24"));
314        assert!(yaml.contains("label: my-label"));
315        assert!(yaml.contains("lifetime: forever"));
316    }
317
318    #[test]
319    #[cfg(feature = "serde")]
320    fn test_address_mapping_complex_deserialize() {
321        let yaml = r#"
322192.168.1.10/24:
323  label: my-label
324  lifetime: forever
325"#;
326
327        let mapping: AddressMapping = serde_yaml::from_str(yaml).unwrap();
328
329        if let AddressMapping::Complex(map) = mapping {
330            let props = map.get("192.168.1.10/24").unwrap();
331            assert_eq!(props.label, Some("my-label".to_string()));
332            assert_eq!(props.lifetime, Some(PreferredLifetime::Forever));
333        } else {
334            panic!("Expected Complex variant");
335        }
336    }
337
338    #[test]
339    fn test_address_properties_defaults() {
340        let props = AddressProperties::default();
341        assert_eq!(props.label, None);
342        assert_eq!(props.lifetime, None);
343    }
344
345    #[test]
346    #[cfg(feature = "serde")]
347    fn test_address_properties_lifetime_forever() {
348        let yaml = r#"
349label: test
350lifetime: forever
351"#;
352
353        let props: AddressProperties = serde_yaml::from_str(yaml).unwrap();
354        assert_eq!(props.label, Some("test".to_string()));
355        assert_eq!(props.lifetime, Some(PreferredLifetime::Forever));
356    }
357
358    #[test]
359    #[cfg(feature = "serde")]
360    fn test_address_properties_lifetime_zero() {
361        let yaml = r#"
362label: test
363lifetime: "0"
364"#;
365
366        let props: AddressProperties = serde_yaml::from_str(yaml).unwrap();
367        assert_eq!(props.label, Some("test".to_string()));
368        assert_eq!(props.lifetime, Some(PreferredLifetime::Zero));
369    }
370
371    #[test]
372    #[cfg(feature = "serde")]
373    fn test_address_properties_skip_none() {
374        let props = AddressProperties {
375            label: Some("test".to_string()),
376            lifetime: None,
377        };
378
379        let yaml = serde_yaml::to_string(&props).unwrap();
380        assert!(yaml.contains("label: test"));
381        assert!(!yaml.contains("lifetime"));
382    }
383
384    #[test]
385    #[cfg(feature = "serde")]
386    fn test_address_properties_label_only() {
387        let yaml = r#"
388label: management
389"#;
390
391        let props: AddressProperties = serde_yaml::from_str(yaml).unwrap();
392        assert_eq!(props.label, Some("management".to_string()));
393        assert_eq!(props.lifetime, None);
394    }
395
396    #[test]
397    #[cfg(feature = "serde")]
398    fn test_use_domains_boolean() {
399        let yaml = r#"
400use-domains: true
401"#;
402
403        let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
404        assert_eq!(overrides.use_domains, Some("true".to_string()));
405    }
406
407    #[test]
408    #[cfg(feature = "serde")]
409    fn test_use_domains_route() {
410        let yaml = r#"
411use-domains: route
412"#;
413
414        let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
415        assert_eq!(overrides.use_domains, Some("route".to_string()));
416    }
417
418    #[test]
419    #[cfg(feature = "serde")]
420    fn test_address_mapping_list() {
421        let yaml = r#"
422- 192.168.1.10/24
423- 192.168.1.11/24:
424    label: backup
425    lifetime: "0"
426- 10.0.0.1/8
427"#;
428
429        let mappings: Vec<AddressMapping> = serde_yaml::from_str(yaml).unwrap();
430        assert_eq!(mappings.len(), 3);
431
432        // First should be simple
433        assert!(matches!(mappings[0], AddressMapping::Simple(_)));
434
435        // Second should be complex
436        if let AddressMapping::Complex(map) = &mappings[1] {
437            let props = map.get("192.168.1.11/24").unwrap();
438            assert_eq!(props.label, Some("backup".to_string()));
439            assert_eq!(props.lifetime, Some(PreferredLifetime::Zero));
440        } else {
441            panic!("Expected Complex variant");
442        }
443
444        // Third should be simple
445        assert!(matches!(mappings[2], AddressMapping::Simple(_)));
446    }
447
448    #[test]
449    fn test_ipv6_address_generation_equality() {
450        assert_eq!(Ipv6AddressGeneration::Eui64, Ipv6AddressGeneration::Eui64);
451        assert_eq!(
452            Ipv6AddressGeneration::StablePrivacy,
453            Ipv6AddressGeneration::StablePrivacy
454        );
455        assert_ne!(
456            Ipv6AddressGeneration::Eui64,
457            Ipv6AddressGeneration::StablePrivacy
458        );
459    }
460
461    #[test]
462    #[cfg(feature = "serde")]
463    fn test_route_metric_range() {
464        let yaml = r#"
465route-metric: 65535
466"#;
467
468        let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
469        assert_eq!(overrides.route_metric, Some(65535));
470    }
471
472    #[test]
473    #[cfg(feature = "serde")]
474    fn test_empty_dhcp_overrides() {
475        let yaml = "{}";
476        let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
477
478        assert_eq!(overrides.use_dns, None);
479        assert_eq!(overrides.use_ntp, None);
480        assert_eq!(overrides.route_metric, None);
481    }
482}