Skip to main content

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<crate::UseDomains>,
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    #[cfg(feature = "serde")]
168    use crate::UseDomains;
169
170    #[test]
171    fn test_dhcp_overrides_defaults() {
172        let overrides = DhcpOverrides::default();
173        assert_eq!(overrides.use_dns, None);
174        assert_eq!(overrides.use_ntp, None);
175        assert_eq!(overrides.send_hostname, None);
176        assert_eq!(overrides.use_hostname, None);
177        assert_eq!(overrides.use_mtu, None);
178        assert_eq!(overrides.hostname, None);
179        assert_eq!(overrides.use_routes, None);
180        assert_eq!(overrides.route_metric, None);
181        assert_eq!(overrides.use_domains, None);
182    }
183
184    #[test]
185    #[cfg(feature = "serde")]
186    fn test_dhcp_overrides_serialize() {
187        let overrides = DhcpOverrides {
188            use_dns: Some(false),
189            use_ntp: Some(true),
190            send_hostname: Some(true),
191            use_hostname: Some(false),
192            use_mtu: Some(true),
193            hostname: Some("test-host".to_string()),
194            use_routes: Some(false),
195            route_metric: Some(100),
196            use_domains: Some(UseDomains::Route),
197        };
198
199        let yaml = serde_yaml::to_string(&overrides).unwrap();
200        assert!(yaml.contains("use-dns: false"));
201        assert!(yaml.contains("use-ntp: true"));
202        assert!(yaml.contains("send-hostname: true"));
203        assert!(yaml.contains("use-hostname: false"));
204        assert!(yaml.contains("use-mtu: true"));
205        assert!(yaml.contains("hostname: test-host"));
206        assert!(yaml.contains("use-routes: false"));
207        assert!(yaml.contains("route-metric: 100"));
208        assert!(yaml.contains("use-domains: route"));
209    }
210
211    #[test]
212    #[cfg(feature = "serde")]
213    fn test_dhcp_overrides_deserialize() {
214        let yaml = r#"
215use-dns: false
216use-ntp: true
217send-hostname: true
218use-hostname: false
219use-mtu: true
220hostname: test-host
221use-routes: false
222route-metric: 100
223use-domains: route
224"#;
225
226        let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
227        assert_eq!(overrides.use_dns, Some(false));
228        assert_eq!(overrides.use_ntp, Some(true));
229        assert_eq!(overrides.send_hostname, Some(true));
230        assert_eq!(overrides.use_hostname, Some(false));
231        assert_eq!(overrides.use_mtu, Some(true));
232        assert_eq!(overrides.hostname, Some("test-host".to_string()));
233        assert_eq!(overrides.use_routes, Some(false));
234        assert_eq!(overrides.route_metric, Some(100));
235        assert_eq!(overrides.use_domains, Some(UseDomains::Route));
236    }
237
238    #[test]
239    #[cfg(feature = "serde")]
240    fn test_dhcp_overrides_skip_none_serialization() {
241        let overrides = DhcpOverrides {
242            use_dns: Some(false),
243            use_ntp: None,
244            ..Default::default()
245        };
246
247        let yaml = serde_yaml::to_string(&overrides).unwrap();
248        assert!(yaml.contains("use-dns: false"));
249        assert!(!yaml.contains("use-ntp"));
250        assert!(!yaml.contains("send-hostname"));
251    }
252
253    #[test]
254    #[cfg(feature = "serde")]
255    fn test_dhcp_overrides_bool_as_string() {
256        let yaml = r#"
257use-dns: "false"
258use-ntp: "true"
259"#;
260
261        let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
262        assert_eq!(overrides.use_dns, Some(false));
263        assert_eq!(overrides.use_ntp, Some(true));
264    }
265
266    #[test]
267    #[cfg(feature = "serde")]
268    fn test_ipv6_address_generation_serialize() {
269        let eui64 = Ipv6AddressGeneration::Eui64;
270        let stable = Ipv6AddressGeneration::StablePrivacy;
271
272        let eui64_yaml = serde_yaml::to_string(&eui64).unwrap();
273        let stable_yaml = serde_yaml::to_string(&stable).unwrap();
274
275        assert!(eui64_yaml.contains("eui64"));
276        assert!(stable_yaml.contains("stable-privacy"));
277    }
278
279    #[test]
280    #[cfg(feature = "serde")]
281    fn test_ipv6_address_generation_deserialize() {
282        let eui64: Ipv6AddressGeneration = serde_yaml::from_str("eui64").unwrap();
283        let stable: Ipv6AddressGeneration = serde_yaml::from_str("stable-privacy").unwrap();
284
285        assert_eq!(eui64, Ipv6AddressGeneration::Eui64);
286        assert_eq!(stable, Ipv6AddressGeneration::StablePrivacy);
287    }
288
289    #[test]
290    #[cfg(feature = "serde")]
291    fn test_address_mapping_simple() {
292        let simple = AddressMapping::Simple("192.168.1.10/24".to_string());
293
294        let yaml = serde_yaml::to_string(&simple).unwrap();
295        assert_eq!(yaml.trim(), "192.168.1.10/24");
296
297        let deserialized: AddressMapping = serde_yaml::from_str("192.168.1.10/24").unwrap();
298        assert_eq!(deserialized, simple);
299    }
300
301    #[test]
302    #[cfg(feature = "serde")]
303    fn test_address_mapping_complex() {
304        let mut map = std::collections::HashMap::new();
305        map.insert(
306            "192.168.1.10/24".to_string(),
307            AddressProperties {
308                label: Some("my-label".to_string()),
309                lifetime: Some(PreferredLifetime::Forever),
310            },
311        );
312        let complex = AddressMapping::Complex(map);
313
314        let yaml = serde_yaml::to_string(&complex).unwrap();
315        assert!(yaml.contains("192.168.1.10/24"));
316        assert!(yaml.contains("label: my-label"));
317        assert!(yaml.contains("lifetime: forever"));
318    }
319
320    #[test]
321    #[cfg(feature = "serde")]
322    fn test_address_mapping_complex_deserialize() {
323        let yaml = r#"
324192.168.1.10/24:
325  label: my-label
326  lifetime: forever
327"#;
328
329        let mapping: AddressMapping = serde_yaml::from_str(yaml).unwrap();
330
331        if let AddressMapping::Complex(map) = mapping {
332            let props = map.get("192.168.1.10/24").unwrap();
333            assert_eq!(props.label, Some("my-label".to_string()));
334            assert_eq!(props.lifetime, Some(PreferredLifetime::Forever));
335        } else {
336            panic!("Expected Complex variant");
337        }
338    }
339
340    #[test]
341    fn test_address_properties_defaults() {
342        let props = AddressProperties::default();
343        assert_eq!(props.label, None);
344        assert_eq!(props.lifetime, None);
345    }
346
347    #[test]
348    #[cfg(feature = "serde")]
349    fn test_address_properties_lifetime_forever() {
350        let yaml = r#"
351label: test
352lifetime: forever
353"#;
354
355        let props: AddressProperties = serde_yaml::from_str(yaml).unwrap();
356        assert_eq!(props.label, Some("test".to_string()));
357        assert_eq!(props.lifetime, Some(PreferredLifetime::Forever));
358    }
359
360    #[test]
361    #[cfg(feature = "serde")]
362    fn test_address_properties_lifetime_zero() {
363        let yaml = r#"
364label: test
365lifetime: "0"
366"#;
367
368        let props: AddressProperties = serde_yaml::from_str(yaml).unwrap();
369        assert_eq!(props.label, Some("test".to_string()));
370        assert_eq!(props.lifetime, Some(PreferredLifetime::Zero));
371    }
372
373    #[test]
374    #[cfg(feature = "serde")]
375    fn test_address_properties_skip_none() {
376        let props = AddressProperties {
377            label: Some("test".to_string()),
378            lifetime: None,
379        };
380
381        let yaml = serde_yaml::to_string(&props).unwrap();
382        assert!(yaml.contains("label: test"));
383        assert!(!yaml.contains("lifetime"));
384    }
385
386    #[test]
387    #[cfg(feature = "serde")]
388    fn test_address_properties_label_only() {
389        let yaml = r#"
390label: management
391"#;
392
393        let props: AddressProperties = serde_yaml::from_str(yaml).unwrap();
394        assert_eq!(props.label, Some("management".to_string()));
395        assert_eq!(props.lifetime, None);
396    }
397
398    #[test]
399    #[cfg(feature = "serde")]
400    fn test_use_domains_boolean() {
401        let yaml = r#"
402use-domains: true
403"#;
404
405        let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
406        assert_eq!(overrides.use_domains, Some(UseDomains::Boolean(true)));
407    }
408
409    #[test]
410    #[cfg(feature = "serde")]
411    fn test_use_domains_boolean_quoted() {
412        let yaml = r#"
413use-domains: "false"
414"#;
415
416        let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
417        assert_eq!(overrides.use_domains, Some(UseDomains::Boolean(false)));
418    }
419
420    #[test]
421    #[cfg(feature = "serde")]
422    fn test_use_domains_route() {
423        let yaml = r#"
424use-domains: route
425"#;
426
427        let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
428        assert_eq!(overrides.use_domains, Some(UseDomains::Route));
429    }
430
431    #[test]
432    #[cfg(feature = "serde")]
433    fn test_use_domains_serialize_roundtrip() {
434        let boolean = DhcpOverrides {
435            use_domains: Some(UseDomains::Boolean(true)),
436            ..Default::default()
437        };
438        let yaml = serde_yaml::to_string(&boolean).unwrap();
439        assert!(yaml.contains("use-domains: true"));
440        assert_eq!(
441            serde_yaml::from_str::<DhcpOverrides>(&yaml).unwrap(),
442            boolean
443        );
444
445        let route = DhcpOverrides {
446            use_domains: Some(UseDomains::Route),
447            ..Default::default()
448        };
449        let yaml = serde_yaml::to_string(&route).unwrap();
450        assert!(yaml.contains("use-domains: route"));
451        assert_eq!(serde_yaml::from_str::<DhcpOverrides>(&yaml).unwrap(), route);
452    }
453
454    #[test]
455    #[cfg(feature = "serde")]
456    fn test_address_mapping_list() {
457        let yaml = r#"
458- 192.168.1.10/24
459- 192.168.1.11/24:
460    label: backup
461    lifetime: "0"
462- 10.0.0.1/8
463"#;
464
465        let mappings: Vec<AddressMapping> = serde_yaml::from_str(yaml).unwrap();
466        assert_eq!(mappings.len(), 3);
467
468        // First should be simple
469        assert!(matches!(mappings[0], AddressMapping::Simple(_)));
470
471        // Second should be complex
472        if let AddressMapping::Complex(map) = &mappings[1] {
473            let props = map.get("192.168.1.11/24").unwrap();
474            assert_eq!(props.label, Some("backup".to_string()));
475            assert_eq!(props.lifetime, Some(PreferredLifetime::Zero));
476        } else {
477            panic!("Expected Complex variant");
478        }
479
480        // Third should be simple
481        assert!(matches!(mappings[2], AddressMapping::Simple(_)));
482    }
483
484    #[test]
485    fn test_ipv6_address_generation_equality() {
486        assert_eq!(Ipv6AddressGeneration::Eui64, Ipv6AddressGeneration::Eui64);
487        assert_eq!(
488            Ipv6AddressGeneration::StablePrivacy,
489            Ipv6AddressGeneration::StablePrivacy
490        );
491        assert_ne!(
492            Ipv6AddressGeneration::Eui64,
493            Ipv6AddressGeneration::StablePrivacy
494        );
495    }
496
497    #[test]
498    #[cfg(feature = "serde")]
499    fn test_route_metric_range() {
500        let yaml = r#"
501route-metric: 65535
502"#;
503
504        let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
505        assert_eq!(overrides.route_metric, Some(65535));
506    }
507
508    #[test]
509    #[cfg(feature = "serde")]
510    fn test_empty_dhcp_overrides() {
511        let yaml = "{}";
512        let overrides: DhcpOverrides = serde_yaml::from_str(yaml).unwrap();
513
514        assert_eq!(overrides.use_dns, None);
515        assert_eq!(overrides.use_ntp, None);
516        assert_eq!(overrides.route_metric, None);
517    }
518}