]>
Commit | Line | Data |
---|---|---|
f34d4401 DM |
1 | use std::io::{Write}; |
2 | use std::collections::{HashSet, HashMap}; | |
3 | ||
df6bb03d | 4 | use anyhow::{Error, format_err, bail}; |
bab5d18c | 5 | use serde::de::{value, IntoDeserializer, Deserialize}; |
f34d4401 | 6 | |
904e9886 DM |
7 | use proxmox::tools::{fs::replace_file, fs::CreateOptions}; |
8 | ||
f34d4401 | 9 | mod helper; |
92310d58 | 10 | pub use helper::*; |
f34d4401 DM |
11 | |
12 | mod lexer; | |
13 | pub use lexer::*; | |
14 | ||
15 | mod parser; | |
16 | pub use parser::*; | |
17 | ||
bab5d18c DM |
18 | use crate::api2::types::{Interface, NetworkConfigMethod, NetworkInterfaceType, LinuxBondMode}; |
19 | ||
20 | pub fn bond_mode_from_str(s: &str) -> Result<LinuxBondMode, Error> { | |
21 | LinuxBondMode::deserialize(s.into_deserializer()) | |
22 | .map_err(|_: value::Error| format_err!("invalid bond_mode '{}'", s)) | |
23 | } | |
24 | ||
25 | pub fn bond_mode_to_str(mode: LinuxBondMode) -> &'static str { | |
26 | match mode { | |
27 | LinuxBondMode::balance_rr => "balance-rr", | |
28 | LinuxBondMode::active_backup => "active-backup", | |
29 | LinuxBondMode::balance_xor => "balance-xor", | |
30 | LinuxBondMode::broadcast => "broadcast", | |
31 | LinuxBondMode::ieee802_3ad => "802.3ad", | |
32 | LinuxBondMode::balance_tlb => "balance-tlb", | |
33 | LinuxBondMode::balance_alb => "balance-alb", | |
34 | } | |
35 | } | |
f34d4401 DM |
36 | |
37 | impl Interface { | |
38 | ||
92310d58 | 39 | pub fn new(name: String) -> Self { |
a9bb491e | 40 | Self { |
f34d4401 | 41 | name, |
02269f3d | 42 | interface_type: NetworkInterfaceType::Unknown, |
7b22acd0 | 43 | autostart: false, |
3f129233 | 44 | active: false, |
7b22acd0 DM |
45 | method: None, |
46 | method6: None, | |
47 | cidr: None, | |
48 | gateway: None, | |
49 | cidr6: None, | |
50 | gateway6: None, | |
51 | options: Vec::new(), | |
52 | options6: Vec::new(), | |
53 | comments: None, | |
54 | comments6: None, | |
2c18efd9 | 55 | mtu: None, |
1d9a68c2 | 56 | bridge_ports: None, |
7b22acd0 | 57 | bridge_vlan_aware: None, |
bab5d18c DM |
58 | slaves: None, |
59 | bond_mode: None, | |
f34d4401 DM |
60 | } |
61 | } | |
62 | ||
7e02d08c | 63 | fn set_method_v4(&mut self, method: NetworkConfigMethod) -> Result<(), Error> { |
7b22acd0 DM |
64 | if self.method.is_none() { |
65 | self.method = Some(method); | |
92310d58 DM |
66 | } else { |
67 | bail!("inet configuration method already set."); | |
68 | } | |
69 | Ok(()) | |
70 | } | |
71 | ||
7e02d08c | 72 | fn set_method_v6(&mut self, method: NetworkConfigMethod) -> Result<(), Error> { |
7b22acd0 DM |
73 | if self.method6.is_none() { |
74 | self.method6 = Some(method); | |
92310d58 DM |
75 | } else { |
76 | bail!("inet6 configuration method already set."); | |
77 | } | |
78 | Ok(()) | |
79 | } | |
80 | ||
8b57cd44 | 81 | fn set_cidr_v4(&mut self, address: String) -> Result<(), Error> { |
7b22acd0 DM |
82 | if self.cidr.is_none() { |
83 | self.cidr = Some(address); | |
f34d4401 DM |
84 | } else { |
85 | bail!("duplicate IPv4 address."); | |
86 | } | |
87 | Ok(()) | |
88 | } | |
89 | ||
90 | fn set_gateway_v4(&mut self, gateway: String) -> Result<(), Error> { | |
7b22acd0 DM |
91 | if self.gateway.is_none() { |
92 | self.gateway = Some(gateway); | |
f34d4401 DM |
93 | } else { |
94 | bail!("duplicate IPv4 gateway."); | |
95 | } | |
96 | Ok(()) | |
97 | } | |
98 | ||
8b57cd44 | 99 | fn set_cidr_v6(&mut self, address: String) -> Result<(), Error> { |
7b22acd0 DM |
100 | if self.cidr6.is_none() { |
101 | self.cidr6 = Some(address); | |
f34d4401 DM |
102 | } else { |
103 | bail!("duplicate IPv6 address."); | |
104 | } | |
105 | Ok(()) | |
106 | } | |
107 | ||
108 | fn set_gateway_v6(&mut self, gateway: String) -> Result<(), Error> { | |
7b22acd0 DM |
109 | if self.gateway6.is_none() { |
110 | self.gateway6 = Some(gateway); | |
f34d4401 DM |
111 | } else { |
112 | bail!("duplicate IPv4 gateway."); | |
113 | } | |
114 | Ok(()) | |
115 | } | |
116 | ||
c38b4bb8 DM |
117 | fn set_interface_type(&mut self, interface_type: NetworkInterfaceType) -> Result<(), Error> { |
118 | if self.interface_type == NetworkInterfaceType::Unknown { | |
119 | self.interface_type = interface_type; | |
120 | } else if self.interface_type != interface_type { | |
121 | bail!("interface type already defined - cannot change from {:?} to {:?}", self.interface_type, interface_type); | |
122 | } | |
123 | Ok(()) | |
124 | } | |
125 | ||
5e4e88e8 DM |
126 | pub(crate) fn set_bridge_ports(&mut self, ports: Vec<String>) -> Result<(), Error> { |
127 | if self.interface_type != NetworkInterfaceType::Bridge { | |
128 | bail!("interface '{}' is no bridge (type is {:?})", self.name, self.interface_type); | |
129 | } | |
130 | self.bridge_ports = Some(ports); | |
131 | Ok(()) | |
132 | } | |
133 | ||
134 | pub(crate) fn set_bond_slaves(&mut self, slaves: Vec<String>) -> Result<(), Error> { | |
135 | if self.interface_type != NetworkInterfaceType::Bond { | |
136 | bail!("interface '{}' is no bond (type is {:?})", self.name, self.interface_type); | |
137 | } | |
bab5d18c | 138 | self.slaves = Some(slaves); |
5e4e88e8 DM |
139 | Ok(()) |
140 | } | |
141 | ||
2c18efd9 DM |
142 | /// Write attributes not dependening on address family |
143 | fn write_iface_attributes(&self, w: &mut dyn Write) -> Result<(), Error> { | |
1d9a68c2 DM |
144 | |
145 | match self.interface_type { | |
146 | NetworkInterfaceType::Bridge => { | |
7b22acd0 DM |
147 | if let Some(true) = self.bridge_vlan_aware { |
148 | writeln!(w, "\tbridge-vlan-aware yes")?; | |
149 | } | |
1d9a68c2 DM |
150 | if let Some(ref ports) = self.bridge_ports { |
151 | if ports.is_empty() { | |
7b22acd0 | 152 | writeln!(w, "\tbridge-ports none")?; |
1d9a68c2 | 153 | } else { |
7b22acd0 | 154 | writeln!(w, "\tbridge-ports {}", ports.join(" "))?; |
1d9a68c2 DM |
155 | } |
156 | } | |
157 | } | |
42fbe91a | 158 | NetworkInterfaceType::Bond => { |
bab5d18c DM |
159 | let mode = self.bond_mode.unwrap_or(LinuxBondMode::balance_rr); |
160 | writeln!(w, "\tbond-mode {}", bond_mode_to_str(mode))?; | |
161 | ||
162 | if let Some(ref slaves) = self.slaves { | |
42fbe91a | 163 | if slaves.is_empty() { |
7b22acd0 | 164 | writeln!(w, "\tbond-slaves none")?; |
42fbe91a | 165 | } else { |
7b22acd0 | 166 | writeln!(w, "\tbond-slaves {}", slaves.join(" "))?; |
42fbe91a DM |
167 | } |
168 | } | |
169 | } | |
1d9a68c2 DM |
170 | _ => {} |
171 | } | |
172 | ||
2c18efd9 | 173 | if let Some(mtu) = self.mtu { |
7b22acd0 | 174 | writeln!(w, "\tmtu {}", mtu)?; |
2c18efd9 | 175 | } |
1d9a68c2 | 176 | |
2c18efd9 DM |
177 | Ok(()) |
178 | } | |
179 | ||
180 | /// Write attributes dependening on address family inet (IPv4) | |
f8e7ac68 DM |
181 | fn write_iface_attributes_v4(&self, w: &mut dyn Write, method: NetworkConfigMethod) -> Result<(), Error> { |
182 | if method == NetworkConfigMethod::Static { | |
7b22acd0 DM |
183 | if let Some(address) = &self.cidr { |
184 | writeln!(w, "\taddress {}", address)?; | |
f8e7ac68 | 185 | } |
7b22acd0 DM |
186 | if let Some(gateway) = &self.gateway { |
187 | writeln!(w, "\tgateway {}", gateway)?; | |
f8e7ac68 | 188 | } |
a9bb491e | 189 | } |
f8e7ac68 | 190 | |
7b22acd0 DM |
191 | for option in &self.options { |
192 | writeln!(w, "\t{}", option)?; | |
a9bb491e DM |
193 | } |
194 | ||
7b22acd0 | 195 | if let Some(ref comments) = self.comments { |
8a6b86b8 DM |
196 | for comment in comments.lines() { |
197 | writeln!(w, "#{}", comment)?; | |
198 | } | |
5f60a58f DM |
199 | } |
200 | ||
a9bb491e DM |
201 | Ok(()) |
202 | } | |
203 | ||
2c18efd9 | 204 | /// Write attributes dependening on address family inet6 (IPv6) |
f8e7ac68 DM |
205 | fn write_iface_attributes_v6(&self, w: &mut dyn Write, method: NetworkConfigMethod) -> Result<(), Error> { |
206 | if method == NetworkConfigMethod::Static { | |
7b22acd0 DM |
207 | if let Some(address) = &self.cidr6 { |
208 | writeln!(w, "\taddress {}", address)?; | |
f8e7ac68 | 209 | } |
7b22acd0 DM |
210 | if let Some(gateway) = &self.gateway6 { |
211 | writeln!(w, "\tgateway {}", gateway)?; | |
f8e7ac68 | 212 | } |
a9bb491e | 213 | } |
f8e7ac68 | 214 | |
7b22acd0 DM |
215 | for option in &self.options6 { |
216 | writeln!(w, "\t{}", option)?; | |
a9bb491e DM |
217 | } |
218 | ||
7b22acd0 | 219 | if let Some(ref comments) = self.comments6 { |
8a6b86b8 DM |
220 | for comment in comments.lines() { |
221 | writeln!(w, "#{}", comment)?; | |
222 | } | |
5f60a58f DM |
223 | } |
224 | ||
a9bb491e DM |
225 | Ok(()) |
226 | } | |
227 | ||
d5ca9bd5 DM |
228 | /// Return whether we can write a single entry for inet and inet6 |
229 | fn combine_entry(&self) -> bool { | |
230 | // Note: use match to make sure we considered all values at compile time | |
231 | match self { | |
232 | Interface { | |
7b22acd0 DM |
233 | method, |
234 | method6, | |
235 | options, | |
236 | options6, | |
237 | comments, | |
238 | comments6, | |
d5ca9bd5 DM |
239 | // the rest does not matter |
240 | name: _name, | |
02269f3d | 241 | interface_type: _interface_type, |
7b22acd0 | 242 | autostart: _autostart, |
d5ca9bd5 | 243 | active: _active, |
7b22acd0 DM |
244 | cidr: _cidr, |
245 | cidr6: _cidr6, | |
246 | gateway: _gateway, | |
247 | gateway6: _gateway6, | |
d5ca9bd5 | 248 | mtu: _mtu, |
1d9a68c2 | 249 | bridge_ports: _bridge_ports, |
7b22acd0 | 250 | bridge_vlan_aware: _bridge_vlan_aware, |
bab5d18c DM |
251 | slaves: _slaves, |
252 | bond_mode: _bond_mode, | |
d5ca9bd5 | 253 | } => { |
7b22acd0 DM |
254 | method == method6 |
255 | && comments.is_none() | |
256 | && comments6.is_none() | |
257 | && options.is_empty() | |
258 | && options6.is_empty() | |
d5ca9bd5 DM |
259 | } |
260 | } | |
261 | } | |
262 | ||
a9bb491e DM |
263 | fn write_iface(&self, w: &mut dyn Write) -> Result<(), Error> { |
264 | ||
7e02d08c | 265 | fn method_to_str(method: NetworkConfigMethod) -> &'static str { |
a9bb491e | 266 | match method { |
7e02d08c DM |
267 | NetworkConfigMethod::Static => "static", |
268 | NetworkConfigMethod::Loopback => "loopback", | |
269 | NetworkConfigMethod::Manual => "manual", | |
270 | NetworkConfigMethod::DHCP => "dhcp", | |
a9bb491e DM |
271 | } |
272 | } | |
273 | ||
7b22acd0 | 274 | if self.method.is_none() && self.method6.is_none() { return Ok(()); } |
f8e7ac68 | 275 | |
7b22acd0 | 276 | if self.autostart { |
a9bb491e DM |
277 | writeln!(w, "auto {}", self.name)?; |
278 | } | |
279 | ||
d5ca9bd5 | 280 | if self.combine_entry() { |
7b22acd0 | 281 | if let Some(method) = self.method { |
f8e7ac68 DM |
282 | writeln!(w, "iface {} {}", self.name, method_to_str(method))?; |
283 | self.write_iface_attributes_v4(w, method)?; | |
284 | self.write_iface_attributes_v6(w, method)?; | |
2c18efd9 | 285 | self.write_iface_attributes(w)?; |
f8e7ac68 DM |
286 | writeln!(w)?; |
287 | } | |
7b22acd0 DM |
288 | return Ok(()); |
289 | } | |
bab5d18c | 290 | |
7b22acd0 DM |
291 | if let Some(method) = self.method { |
292 | writeln!(w, "iface {} inet {}", self.name, method_to_str(method))?; | |
293 | self.write_iface_attributes_v4(w, method)?; | |
294 | self.write_iface_attributes(w)?; | |
295 | writeln!(w)?; | |
296 | } | |
bab5d18c | 297 | |
7b22acd0 DM |
298 | if let Some(method6) = self.method6 { |
299 | let mut skip_v6 = false; // avoid empty inet6 manual entry | |
300 | if self.method.is_some() && method6 == NetworkConfigMethod::Manual { | |
301 | if self.comments6.is_none() && self.options6.is_empty() { skip_v6 = true; } | |
a9bb491e | 302 | } |
bab5d18c | 303 | |
7b22acd0 DM |
304 | if !skip_v6 { |
305 | writeln!(w, "iface {} inet6 {}", self.name, method_to_str(method6))?; | |
306 | self.write_iface_attributes_v6(w, method6)?; | |
307 | if self.method.is_none() { // only write common attributes once | |
5f60a58f DM |
308 | self.write_iface_attributes(w)?; |
309 | } | |
a9bb491e DM |
310 | writeln!(w)?; |
311 | } | |
312 | } | |
7b22acd0 | 313 | |
a9bb491e DM |
314 | Ok(()) |
315 | } | |
f34d4401 DM |
316 | } |
317 | ||
318 | #[derive(Debug)] | |
319 | enum NetworkOrderEntry { | |
320 | Iface(String), | |
321 | Comment(String), | |
322 | Option(String), | |
323 | } | |
324 | ||
325 | #[derive(Debug)] | |
326 | pub struct NetworkConfig { | |
ca0e5347 | 327 | pub interfaces: HashMap<String, Interface>, |
f34d4401 DM |
328 | order: Vec<NetworkOrderEntry>, |
329 | } | |
330 | ||
1ca540a6 DM |
331 | use std::convert::TryFrom; |
332 | ||
333 | impl TryFrom<NetworkConfig> for String { | |
334 | ||
335 | type Error = Error; | |
336 | ||
337 | fn try_from(config: NetworkConfig) -> Result<Self, Self::Error> { | |
338 | let mut output = Vec::new(); | |
339 | config.write_config(&mut output)?; | |
340 | let res = String::from_utf8(output)?; | |
341 | Ok(res) | |
342 | } | |
343 | } | |
344 | ||
f34d4401 DM |
345 | impl NetworkConfig { |
346 | ||
347 | pub fn new() -> Self { | |
348 | Self { | |
f34d4401 DM |
349 | interfaces: HashMap::new(), |
350 | order: Vec::new(), | |
351 | } | |
352 | } | |
353 | ||
df6bb03d DM |
354 | pub fn lookup(&self, name: &str) -> Result<&Interface, Error> { |
355 | let interface = self.interfaces.get(name).ok_or_else(|| { | |
356 | format_err!("interface '{}' does not exist.", name) | |
357 | })?; | |
358 | Ok(interface) | |
359 | } | |
360 | ||
361 | pub fn lookup_mut(&mut self, name: &str) -> Result<&mut Interface, Error> { | |
362 | let interface = self.interfaces.get_mut(name).ok_or_else(|| { | |
363 | format_err!("interface '{}' does not exist.", name) | |
364 | })?; | |
365 | Ok(interface) | |
366 | } | |
367 | ||
f34d4401 DM |
368 | pub fn write_config(&self, w: &mut dyn Write) -> Result<(), Error> { |
369 | ||
f34d4401 DM |
370 | let mut done = HashSet::new(); |
371 | ||
372 | let mut last_entry_was_comment = false; | |
373 | ||
374 | for entry in self.order.iter() { | |
a9bb491e | 375 | match entry { |
f34d4401 DM |
376 | NetworkOrderEntry::Comment(comment) => { |
377 | writeln!(w, "#{}", comment)?; | |
378 | last_entry_was_comment = true; | |
379 | } | |
380 | NetworkOrderEntry::Option(option) => { | |
381 | if last_entry_was_comment { writeln!(w)?; } | |
382 | last_entry_was_comment = false; | |
383 | writeln!(w, "{}", option)?; | |
384 | writeln!(w)?; | |
385 | } | |
386 | NetworkOrderEntry::Iface(name) => { | |
387 | let interface = match self.interfaces.get(name) { | |
388 | Some(interface) => interface, | |
389 | None => continue, | |
390 | }; | |
391 | ||
392 | if last_entry_was_comment { writeln!(w)?; } | |
393 | last_entry_was_comment = false; | |
394 | ||
395 | if done.contains(name) { continue; } | |
396 | done.insert(name); | |
397 | ||
a9bb491e | 398 | interface.write_iface(w)?; |
f34d4401 DM |
399 | } |
400 | } | |
401 | } | |
402 | ||
403 | for (name, interface) in &self.interfaces { | |
404 | if done.contains(name) { continue; } | |
a9bb491e | 405 | interface.write_iface(w)?; |
f34d4401 DM |
406 | } |
407 | Ok(()) | |
408 | } | |
409 | } | |
904e9886 DM |
410 | |
411 | pub const NETWORK_INTERFACES_FILENAME: &str = "/etc/network/interfaces"; | |
02e36d96 | 412 | pub const NETWORK_INTERFACES_NEW_FILENAME: &str = "/etc/network/interfaces.new"; |
904e9886 DM |
413 | pub const NETWORK_LOCKFILE: &str = "/var/lock/pve-network.lck"; |
414 | ||
02e36d96 | 415 | |
904e9886 | 416 | pub fn config() -> Result<(NetworkConfig, [u8;32]), Error> { |
02e36d96 DM |
417 | let content = std::fs::read(NETWORK_INTERFACES_NEW_FILENAME) |
418 | .or_else(|err| { | |
904e9886 | 419 | if err.kind() == std::io::ErrorKind::NotFound { |
02e36d96 DM |
420 | std::fs::read(NETWORK_INTERFACES_FILENAME) |
421 | .or_else(|err| { | |
422 | if err.kind() == std::io::ErrorKind::NotFound { | |
423 | Ok(Vec::new()) | |
424 | } else { | |
425 | bail!("unable to read '{}' - {}", NETWORK_INTERFACES_FILENAME, err); | |
426 | } | |
427 | }) | |
904e9886 | 428 | } else { |
02e36d96 | 429 | bail!("unable to read '{}' - {}", NETWORK_INTERFACES_NEW_FILENAME, err); |
904e9886 | 430 | } |
02e36d96 DM |
431 | })?; |
432 | ||
904e9886 DM |
433 | |
434 | let digest = openssl::sha::sha256(&content); | |
435 | ||
1ca540a6 | 436 | let existing_interfaces = get_network_interfaces()?; |
904e9886 | 437 | let mut parser = NetworkParser::new(&content[..]); |
1ca540a6 | 438 | let data = parser.parse_interfaces(Some(&existing_interfaces))?; |
904e9886 DM |
439 | |
440 | Ok((data, digest)) | |
441 | } | |
442 | ||
2eefd9ae DM |
443 | pub fn changes() -> Result<String, Error> { |
444 | ||
445 | if !std::path::Path::new(NETWORK_INTERFACES_NEW_FILENAME).exists() { | |
446 | return Ok(String::new()); | |
447 | } | |
448 | ||
449 | compute_file_diff(NETWORK_INTERFACES_FILENAME, NETWORK_INTERFACES_NEW_FILENAME) | |
450 | } | |
451 | ||
904e9886 DM |
452 | pub fn save_config(config: &NetworkConfig) -> Result<(), Error> { |
453 | ||
454 | let mut raw = Vec::new(); | |
455 | config.write_config(&mut raw)?; | |
456 | ||
457 | let mode = nix::sys::stat::Mode::from_bits_truncate(0o0644); | |
458 | // set the correct owner/group/permissions while saving file | |
459 | // owner(rw) = root, group(r)=root, others(r) | |
460 | let options = CreateOptions::new() | |
461 | .perm(mode) | |
462 | .owner(nix::unistd::ROOT) | |
463 | .group(nix::unistd::Gid::from_raw(0)); | |
464 | ||
02e36d96 | 465 | replace_file(NETWORK_INTERFACES_NEW_FILENAME, &raw, options)?; |
904e9886 DM |
466 | |
467 | Ok(()) | |
468 | } | |
df6bb03d DM |
469 | |
470 | // shell completion helper | |
471 | pub fn complete_interface_name(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> { | |
472 | match config() { | |
473 | Ok((data, _digest)) => data.interfaces.keys().map(|id| id.to_string()).collect(), | |
474 | Err(_) => return vec![], | |
475 | } | |
476 | } | |
1ca540a6 DM |
477 | |
478 | #[cfg(test)] | |
479 | mod test { | |
480 | ||
481 | use anyhow::{Error}; | |
482 | ||
483 | use super::*; | |
484 | ||
485 | #[test] | |
486 | fn test_network_config_create_lo_1() -> Result<(), Error> { | |
487 | ||
488 | let input = ""; | |
489 | ||
490 | let mut parser = NetworkParser::new(&input.as_bytes()[..]); | |
491 | ||
492 | let config = parser.parse_interfaces(None)?; | |
493 | ||
494 | let output = String::try_from(config)?; | |
495 | ||
496 | let expected = "auto lo\niface lo inet loopback\n\n"; | |
497 | assert_eq!(output, expected); | |
498 | ||
499 | // run again using output as input | |
500 | let mut parser = NetworkParser::new(&output.as_bytes()[..]); | |
501 | ||
502 | let config = parser.parse_interfaces(None)?; | |
503 | ||
504 | let output = String::try_from(config)?; | |
505 | ||
506 | assert_eq!(output, expected); | |
507 | ||
508 | Ok(()) | |
509 | } | |
510 | ||
511 | #[test] | |
512 | fn test_network_config_create_lo_2() -> Result<(), Error> { | |
513 | ||
514 | let input = "#c1\n\n#c2\n\niface test inet manual\n"; | |
515 | ||
516 | let mut parser = NetworkParser::new(&input.as_bytes()[..]); | |
517 | ||
518 | let config = parser.parse_interfaces(None)?; | |
519 | ||
520 | let output = String::try_from(config)?; | |
521 | ||
522 | // Note: loopback should be added in front of other interfaces | |
523 | let expected = "#c1\n#c2\n\nauto lo\niface lo inet loopback\n\niface test inet manual\n\n"; | |
524 | assert_eq!(output, expected); | |
525 | ||
526 | Ok(()) | |
527 | } | |
528 | } |