-
Notifications
You must be signed in to change notification settings - Fork 124
Expand file tree
/
Copy pathnocloud.rs
More file actions
517 lines (466 loc) · 17.6 KB
/
nocloud.rs
File metadata and controls
517 lines (466 loc) · 17.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
//! Cloud-init NoCloud datasource support for network configuration.
//!
//! This module implements parsing for cloud-init network-config files
//! following the NoCloud datasource specification. It supports both
//! Network Config v1 and v2 formats, in either JSON or YAML format.
//!
//! Reference: https://cloudinit.readthedocs.io/en/latest/reference/datasources/nocloud.html
use crate::network::{self, DhcpSetting, NetworkRoute};
use anyhow::{Context, Result};
use ipnetwork::IpNetwork;
use pnet_base::MacAddr;
use serde::Deserialize;
use slog_scope::warn;
use std::{collections::HashMap, fs::File, io::BufReader, net::IpAddr, path::Path, str::FromStr};
pub fn read_config_file(path: &Path, file: &str) -> Result<Option<BufReader<File>>> {
let filename = path.join(file);
if !filename.exists() {
return Ok(None);
}
let file =
File::open(&filename).with_context(|| format!("failed to open file '{filename:?}'"))?;
Ok(Some(BufReader::new(file)))
}
/// Cloud-init Network Config format wrapper
///
/// This can be either v1 or v2 format
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum NetworkConfig {
V1(NetworkConfigV1),
V2(NetworkConfigV2),
}
/// Network Config v1 format
///
/// Used by cloud-init for network configuration
#[derive(Debug, Deserialize)]
pub struct NetworkConfigV1 {
/// Version number (should be 1)
#[serde(default)]
#[allow(dead_code)]
pub version: Option<u8>,
/// List of network configuration entries
pub config: Vec<NetworkConfigV1Entry>,
}
/// Network Config v1 entry
#[derive(Debug, Deserialize)]
pub struct NetworkConfigV1Entry {
/// Type of network config: "physical", "nameserver", etc.
#[serde(rename = "type")]
pub network_type: String,
/// Interface name
pub name: Option<String>,
/// MAC address
pub mac_address: Option<String>,
/// Static IP addresses
#[serde(default)]
pub address: Vec<String>,
/// Subnet configurations
#[serde(default)]
pub subnets: Vec<NetworkConfigV1Subnet>,
}
/// Route configuration in v1 format
///
/// Supports both the preferred `destination` (CIDR) format and the
/// OpenStack-compatible `network`/`netmask` alias.
#[derive(Debug, Deserialize)]
pub struct RouteConfigV1 {
/// Destination in CIDR notation (preferred)
pub destination: Option<String>,
/// Destination network (OpenStack alias for `destination`)
pub network: Option<String>,
/// Netmask for the destination network (used with `network`)
pub netmask: Option<String>,
/// Gateway address
pub gateway: String,
}
/// Network Config v1 subnet
#[derive(Debug, Deserialize)]
pub struct NetworkConfigV1Subnet {
/// Type of subnet: "static", "dhcp", "dhcp4", "dhcp6", etc.
#[serde(rename = "type")]
pub subnet_type: String,
/// IP address (for static configuration)
pub address: Option<String>,
/// Netmask (for static configuration)
pub netmask: Option<String>,
/// Gateway (for static configuration)
pub gateway: Option<String>,
/// DNS nameservers
#[serde(default)]
pub dns_nameservers: Vec<String>,
/// Routes (for static configuration)
#[serde(default)]
pub routes: Vec<RouteConfigV1>,
}
/// Network Config v2 format
///
/// More modern format used by cloud-init and netplan
#[derive(Debug, Deserialize)]
pub struct NetworkConfigV2 {
/// Version number (should be 2)
#[serde(default)]
#[allow(dead_code)]
pub version: Option<u8>,
/// Ethernet interfaces configuration
#[serde(default)]
pub ethernets: HashMap<String, EthernetConfigV2>,
/// Global nameservers configuration
pub nameservers: Option<NameserversConfig>,
}
/// DHCP overrides configuration
///
/// These fields are parsed for schema compatibility with netplan/cloud-init v2
/// format. Afterburn generates dracut kernel arguments and does not control
/// DHCP client behavior directly — the overrides are consumed by the
/// networking stack (e.g., NetworkManager/systemd-networkd) at boot time.
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct DhcpOverrides {
#[serde(rename = "use-dns", default)]
pub use_dns: Option<bool>,
#[serde(rename = "use-routes", default)]
pub use_routes: Option<bool>,
#[serde(rename = "use-domains", default)]
pub use_domains: Option<bool>,
#[serde(rename = "use-hostname", default)]
pub use_hostname: Option<bool>,
#[serde(rename = "use-ntp", default)]
pub use_ntp: Option<bool>,
#[serde(rename = "route-metric", default)]
pub route_metric: Option<u32>,
}
/// Ethernet interface configuration in v2 format
#[derive(Debug, Deserialize)]
pub struct EthernetConfigV2 {
/// DHCP for IPv4
#[serde(default)]
pub dhcp4: bool,
/// DHCP for IPv6
#[serde(default)]
pub dhcp6: bool,
/// DHCP overrides for IPv4 (parsed for schema compatibility, not acted on by afterburn)
#[serde(rename = "dhcp4-overrides")]
#[allow(dead_code)]
pub dhcp4_overrides: Option<DhcpOverrides>,
/// DHCP overrides for IPv6 (parsed for schema compatibility, not acted on by afterburn)
#[serde(rename = "dhcp6-overrides")]
#[allow(dead_code)]
pub dhcp6_overrides: Option<DhcpOverrides>,
/// Static IP addresses in CIDR notation
#[serde(default)]
pub addresses: Vec<String>,
/// Gateway for IPv4
pub gateway4: Option<String>,
/// Gateway for IPv6
pub gateway6: Option<String>,
/// MAC address
#[serde(rename = "match")]
pub match_config: Option<MatchConfig>,
/// Nameservers configuration
pub nameservers: Option<NameserversConfig>,
/// Routes configuration
#[serde(default)]
pub routes: Vec<RouteConfigV2>,
}
/// Match configuration for identifying interfaces
#[derive(Debug, Deserialize)]
pub struct MatchConfig {
/// MAC address to match
pub macaddress: Option<String>,
/// Interface name to match
#[allow(dead_code)]
pub name: Option<String>,
}
/// Nameservers configuration
#[derive(Debug, Deserialize)]
pub struct NameserversConfig {
/// List of nameserver addresses
#[serde(default)]
pub addresses: Vec<String>,
}
/// Route configuration in v2 format
#[derive(Debug, Deserialize)]
pub struct RouteConfigV2 {
/// Destination network in CIDR notation
pub to: String,
/// Gateway address
pub via: String,
}
impl NetworkConfig {
/// Parse network-config file from a path
///
/// Supports both JSON and YAML formats, automatically detecting which is used
pub fn from_file(path: &Path) -> Result<Option<Self>> {
let network_config_path = path.join("network-config");
if !network_config_path.exists() {
return Ok(None);
}
let file =
File::open(&network_config_path).context("failed to open network-config file")?;
let reader = BufReader::new(file);
// serde_yaml can parse both YAML and JSON
let config: NetworkConfig =
serde_yaml::from_reader(reader).context("failed to parse network-config file")?;
Ok(Some(config))
}
/// Convert to network interfaces
pub fn to_interfaces(&self) -> Result<Vec<network::Interface>> {
match self {
NetworkConfig::V1(v1) => v1.to_interfaces(),
NetworkConfig::V2(v2) => v2.to_interfaces(),
}
}
}
impl NetworkConfigV1 {
/// Convert v1 config to network interfaces
pub fn to_interfaces(&self) -> Result<Vec<network::Interface>> {
let nameservers = self
.config
.iter()
.filter(|config| config.network_type == "nameserver")
.collect::<Vec<_>>();
if nameservers.len() > 1 {
warn!("multiple nameserver entries found, using first one");
}
let mut interfaces = self
.config
.iter()
.filter(|config| config.network_type == "physical")
.map(|entry| entry.to_interface())
.collect::<Result<Vec<_>, _>>()?;
// Collect global nameservers
let global_nameservers: Vec<IpAddr> = if let Some(nameserver) = nameservers.first() {
nameserver
.address
.iter()
.map(|ip| IpAddr::from_str(ip))
.collect::<Result<Vec<IpAddr>, _>>()?
} else {
Vec::new()
};
// Add global nameservers to all interfaces
for iface in &mut interfaces {
iface.nameservers.extend(global_nameservers.iter().copied());
}
Ok(interfaces)
}
}
impl NetworkConfigV1Entry {
/// Convert a v1 config entry to an interface
pub fn to_interface(&self) -> Result<network::Interface> {
if self.network_type != "physical" {
return Err(anyhow::anyhow!(
"cannot convert config to interface: unsupported config type \"{}\"",
self.network_type
));
}
let mut iface = network::Interface {
name: self.name.clone(),
nameservers: vec![],
ip_addresses: vec![],
routes: vec![],
dhcp: None,
mac_address: None,
bond: None,
path: None,
priority: 20,
unmanaged: false,
required_for_online: None,
};
// Process subnets
for subnet in &self.subnets {
// Collect nameservers from subnets
for ns in &subnet.dns_nameservers {
let nameserver = IpAddr::from_str(ns)?;
if !iface.nameservers.contains(&nameserver) {
iface.nameservers.push(nameserver);
}
}
// Handle static configuration
if subnet.subnet_type.contains("static") {
// Static subnet may have an IP address, or just routes/DNS configuration
if let Some(address) = &subnet.address {
if let Some(netmask) = &subnet.netmask {
let ip_addr = IpAddr::from_str(address)?;
// Try to parse netmask as IP address first, then as prefix length
let ip_network = if let Ok(netmask_addr) = IpAddr::from_str(netmask) {
IpNetwork::with_netmask(ip_addr, netmask_addr)?
} else if let Ok(prefix_len) = netmask.parse::<u8>() {
IpNetwork::new(ip_addr, prefix_len)?
} else {
return Err(anyhow::anyhow!(
"Invalid netmask format: {}. Expected IP address or prefix length.",
netmask
));
};
iface.ip_addresses.push(ip_network);
} else {
iface.ip_addresses.push(IpNetwork::from_str(address)?);
}
}
} else if subnet.subnet_type == "dhcp" || subnet.subnet_type == "dhcp4" {
iface.dhcp = match iface.dhcp {
Some(DhcpSetting::V6) => Some(DhcpSetting::Both),
_ => Some(DhcpSetting::V4),
};
} else if subnet.subnet_type == "dhcp6" {
iface.dhcp = match iface.dhcp {
Some(DhcpSetting::V4) => Some(DhcpSetting::Both),
_ => Some(DhcpSetting::V6),
};
} else {
warn!(
"subnet type \"{}\" not supported, ignoring",
subnet.subnet_type
);
}
// Handle routes from subnet
for route in &subnet.routes {
let gateway = IpAddr::from_str(&route.gateway)?;
// Parse destination: prefer `destination` (CIDR), fall back to `network`/`netmask`
let destination = if let Some(dest) = &route.destination {
IpNetwork::from_str(dest)?
} else if let Some(network) = &route.network {
let network_addr = IpAddr::from_str(network)?;
if let Some(netmask) = &route.netmask {
let netmask_addr = IpAddr::from_str(netmask)?;
IpNetwork::with_netmask(network_addr, netmask_addr)?
} else {
return Err(anyhow::anyhow!(
"Route with 'network' field requires 'netmask'"
));
}
} else {
return Err(anyhow::anyhow!(
"Route must have either 'destination' or 'network' field"
));
};
iface.routes.push(NetworkRoute {
destination,
gateway,
});
}
// Handle legacy gateway field only if no explicit routes were defined,
// to avoid duplicate default routes
if subnet.routes.is_empty() {
if let Some(gateway) = &subnet.gateway {
let gateway = IpAddr::from_str(gateway)?;
let destination = if gateway.is_ipv6() {
IpNetwork::from_str("::/0")?
} else {
IpNetwork::from_str("0.0.0.0/0")?
};
iface.routes.push(NetworkRoute {
destination,
gateway,
});
}
}
}
// Set MAC address if available
if let Some(mac) = &self.mac_address {
iface.mac_address = Some(MacAddr::from_str(mac)?);
}
Ok(iface)
}
}
impl NetworkConfigV2 {
/// Convert v2 config to network interfaces
pub fn to_interfaces(&self) -> Result<Vec<network::Interface>> {
let mut interfaces = Vec::new();
for (key, config) in &self.ethernets {
// Determine the interface name:
// - Use the key as name unless there's a MAC match without a name
// - If there's a MAC match and the key looks like an arbitrary ID, set name to None
let interface_name = if config.match_config.is_some() && !key.starts_with("eth") {
None
} else {
Some(key.clone())
};
let mut iface = network::Interface {
name: interface_name,
nameservers: vec![],
ip_addresses: vec![],
routes: vec![],
dhcp: None,
mac_address: None,
bond: None,
path: None,
priority: 20,
unmanaged: false,
required_for_online: None,
};
// Set DHCP
iface.dhcp = match (config.dhcp4, config.dhcp6) {
(true, true) => Some(DhcpSetting::Both),
(true, false) => Some(DhcpSetting::V4),
(false, true) => Some(DhcpSetting::V6),
(false, false) => None,
};
// Set static addresses
for addr_str in &config.addresses {
iface.ip_addresses.push(IpNetwork::from_str(addr_str)?);
}
// Set gateways as default routes
if let Some(gateway4) = &config.gateway4 {
iface.routes.push(NetworkRoute {
destination: IpNetwork::from_str("0.0.0.0/0")?,
gateway: IpAddr::from_str(gateway4)?,
});
}
if let Some(gateway6) = &config.gateway6 {
iface.routes.push(NetworkRoute {
destination: IpNetwork::from_str("::/0")?,
gateway: IpAddr::from_str(gateway6)?,
});
}
// Process explicit routes
for route in &config.routes {
iface.routes.push(NetworkRoute {
destination: IpNetwork::from_str(&route.to)?,
gateway: IpAddr::from_str(&route.via)?,
});
}
// Set nameservers
if let Some(nameservers) = &config.nameservers {
iface.nameservers = nameservers
.addresses
.iter()
.map(|ns| IpAddr::from_str(ns))
.collect::<Result<Vec<_>, _>>()?;
}
// Set MAC address from match config
if let Some(match_config) = &config.match_config {
if let Some(mac) = &match_config.macaddress {
iface.mac_address = Some(MacAddr::from_str(mac)?);
}
}
interfaces.push(iface);
}
// Sort interfaces by name for consistent ordering
// Put named interfaces first, then unnamed ones
interfaces.sort_by(|a, b| match (&a.name, &b.name) {
(Some(name_a), Some(name_b)) => name_a.cmp(name_b),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => std::cmp::Ordering::Equal,
});
// Add global nameservers to all interfaces
if let Some(global_nameservers) = &self.nameservers {
let nameserver_addrs: Vec<IpAddr> = global_nameservers
.addresses
.iter()
.map(|ns| IpAddr::from_str(ns))
.collect::<Result<Vec<_>, _>>()?;
for iface in &mut interfaces {
for ns in &nameserver_addrs {
if !iface.nameservers.contains(ns) {
iface.nameservers.push(*ns);
}
}
}
}
Ok(interfaces)
}
}