]>
Commit | Line | Data |
---|---|---|
f34d4401 | 1 | use std::io::{Write}; |
a4ccb461 | 2 | use std::collections::{HashSet, HashMap, BTreeMap}; |
f34d4401 | 3 | |
df6bb03d | 4 | use anyhow::{Error, format_err, bail}; |
bab5d18c | 5 | use serde::de::{value, IntoDeserializer, Deserialize}; |
5bef0f43 DM |
6 | use lazy_static::lazy_static; |
7 | use regex::Regex; | |
f34d4401 | 8 | |
904e9886 DM |
9 | use proxmox::tools::{fs::replace_file, fs::CreateOptions}; |
10 | ||
f34d4401 | 11 | mod helper; |
92310d58 | 12 | pub use helper::*; |
f34d4401 DM |
13 | |
14 | mod lexer; | |
15 | pub use lexer::*; | |
16 | ||
17 | mod parser; | |
18 | pub use parser::*; | |
19 | ||
8f2f3dd7 | 20 | use crate::api2::types::{Interface, NetworkConfigMethod, NetworkInterfaceType, LinuxBondMode, BondXmitHashPolicy}; |
bab5d18c | 21 | |
0ed9a2b3 DM |
22 | lazy_static!{ |
23 | static ref PHYSICAL_NIC_REGEX: Regex = Regex::new(r"^(?:eth\d+|en[^:.]+|ib\d+)$").unwrap(); | |
24 | } | |
25 | ||
26 | pub fn is_physical_nic(iface: &str) -> bool { | |
27 | PHYSICAL_NIC_REGEX.is_match(iface) | |
28 | } | |
29 | ||
bab5d18c DM |
30 | pub fn bond_mode_from_str(s: &str) -> Result<LinuxBondMode, Error> { |
31 | LinuxBondMode::deserialize(s.into_deserializer()) | |
32 | .map_err(|_: value::Error| format_err!("invalid bond_mode '{}'", s)) | |
33 | } | |
34 | ||
35 | pub fn bond_mode_to_str(mode: LinuxBondMode) -> &'static str { | |
36 | match mode { | |
37 | LinuxBondMode::balance_rr => "balance-rr", | |
38 | LinuxBondMode::active_backup => "active-backup", | |
39 | LinuxBondMode::balance_xor => "balance-xor", | |
40 | LinuxBondMode::broadcast => "broadcast", | |
41 | LinuxBondMode::ieee802_3ad => "802.3ad", | |
42 | LinuxBondMode::balance_tlb => "balance-tlb", | |
43 | LinuxBondMode::balance_alb => "balance-alb", | |
44 | } | |
45 | } | |
f34d4401 | 46 | |
8f2f3dd7 DC |
47 | pub fn bond_xmit_hash_policy_from_str(s: &str) -> Result<BondXmitHashPolicy, Error> { |
48 | BondXmitHashPolicy::deserialize(s.into_deserializer()) | |
49 | .map_err(|_: value::Error| format_err!("invalid bond_xmit_hash_policy '{}'", s)) | |
50 | } | |
51 | ||
52 | pub fn bond_xmit_hash_policy_to_str(policy: &BondXmitHashPolicy) -> &'static str { | |
53 | match policy { | |
54 | BondXmitHashPolicy::layer2 => "layer2", | |
55 | BondXmitHashPolicy::layer2_3 => "layer2+3", | |
56 | BondXmitHashPolicy::layer3_4 => "layer3+4", | |
57 | } | |
58 | } | |
59 | ||
f34d4401 DM |
60 | impl Interface { |
61 | ||
92310d58 | 62 | pub fn new(name: String) -> Self { |
a9bb491e | 63 | Self { |
f34d4401 | 64 | name, |
02269f3d | 65 | interface_type: NetworkInterfaceType::Unknown, |
7b22acd0 | 66 | autostart: false, |
3f129233 | 67 | active: false, |
7b22acd0 DM |
68 | method: None, |
69 | method6: None, | |
70 | cidr: None, | |
71 | gateway: None, | |
72 | cidr6: None, | |
73 | gateway6: None, | |
74 | options: Vec::new(), | |
75 | options6: Vec::new(), | |
76 | comments: None, | |
77 | comments6: None, | |
2c18efd9 | 78 | mtu: None, |
1d9a68c2 | 79 | bridge_ports: None, |
7b22acd0 | 80 | bridge_vlan_aware: None, |
bab5d18c DM |
81 | slaves: None, |
82 | bond_mode: None, | |
85959a99 | 83 | bond_primary: None, |
8f2f3dd7 | 84 | bond_xmit_hash_policy: None, |
f34d4401 DM |
85 | } |
86 | } | |
87 | ||
7e02d08c | 88 | fn set_method_v4(&mut self, method: NetworkConfigMethod) -> Result<(), Error> { |
7b22acd0 DM |
89 | if self.method.is_none() { |
90 | self.method = Some(method); | |
92310d58 DM |
91 | } else { |
92 | bail!("inet configuration method already set."); | |
93 | } | |
94 | Ok(()) | |
95 | } | |
96 | ||
7e02d08c | 97 | fn set_method_v6(&mut self, method: NetworkConfigMethod) -> Result<(), Error> { |
7b22acd0 DM |
98 | if self.method6.is_none() { |
99 | self.method6 = Some(method); | |
92310d58 DM |
100 | } else { |
101 | bail!("inet6 configuration method already set."); | |
102 | } | |
103 | Ok(()) | |
104 | } | |
105 | ||
8b57cd44 | 106 | fn set_cidr_v4(&mut self, address: String) -> Result<(), Error> { |
7b22acd0 DM |
107 | if self.cidr.is_none() { |
108 | self.cidr = Some(address); | |
f34d4401 DM |
109 | } else { |
110 | bail!("duplicate IPv4 address."); | |
111 | } | |
112 | Ok(()) | |
113 | } | |
114 | ||
115 | fn set_gateway_v4(&mut self, gateway: String) -> Result<(), Error> { | |
7b22acd0 DM |
116 | if self.gateway.is_none() { |
117 | self.gateway = Some(gateway); | |
f34d4401 DM |
118 | } else { |
119 | bail!("duplicate IPv4 gateway."); | |
120 | } | |
121 | Ok(()) | |
122 | } | |
123 | ||
8b57cd44 | 124 | fn set_cidr_v6(&mut self, address: String) -> Result<(), Error> { |
7b22acd0 DM |
125 | if self.cidr6.is_none() { |
126 | self.cidr6 = Some(address); | |
f34d4401 DM |
127 | } else { |
128 | bail!("duplicate IPv6 address."); | |
129 | } | |
130 | Ok(()) | |
131 | } | |
132 | ||
133 | fn set_gateway_v6(&mut self, gateway: String) -> Result<(), Error> { | |
7b22acd0 DM |
134 | if self.gateway6.is_none() { |
135 | self.gateway6 = Some(gateway); | |
f34d4401 DM |
136 | } else { |
137 | bail!("duplicate IPv4 gateway."); | |
138 | } | |
139 | Ok(()) | |
140 | } | |
141 | ||
c38b4bb8 DM |
142 | fn set_interface_type(&mut self, interface_type: NetworkInterfaceType) -> Result<(), Error> { |
143 | if self.interface_type == NetworkInterfaceType::Unknown { | |
144 | self.interface_type = interface_type; | |
145 | } else if self.interface_type != interface_type { | |
146 | bail!("interface type already defined - cannot change from {:?} to {:?}", self.interface_type, interface_type); | |
147 | } | |
148 | Ok(()) | |
149 | } | |
150 | ||
5e4e88e8 DM |
151 | pub(crate) fn set_bridge_ports(&mut self, ports: Vec<String>) -> Result<(), Error> { |
152 | if self.interface_type != NetworkInterfaceType::Bridge { | |
153 | bail!("interface '{}' is no bridge (type is {:?})", self.name, self.interface_type); | |
154 | } | |
155 | self.bridge_ports = Some(ports); | |
156 | Ok(()) | |
157 | } | |
158 | ||
159 | pub(crate) fn set_bond_slaves(&mut self, slaves: Vec<String>) -> Result<(), Error> { | |
160 | if self.interface_type != NetworkInterfaceType::Bond { | |
161 | bail!("interface '{}' is no bond (type is {:?})", self.name, self.interface_type); | |
162 | } | |
bab5d18c | 163 | self.slaves = Some(slaves); |
5e4e88e8 DM |
164 | Ok(()) |
165 | } | |
166 | ||
add5861e | 167 | /// Write attributes not depending on address family |
2c18efd9 | 168 | fn write_iface_attributes(&self, w: &mut dyn Write) -> Result<(), Error> { |
1d9a68c2 | 169 | |
db5672e8 DM |
170 | static EMPTY_LIST: Vec<String> = Vec::new(); |
171 | ||
1d9a68c2 DM |
172 | match self.interface_type { |
173 | NetworkInterfaceType::Bridge => { | |
7b22acd0 DM |
174 | if let Some(true) = self.bridge_vlan_aware { |
175 | writeln!(w, "\tbridge-vlan-aware yes")?; | |
176 | } | |
db5672e8 DM |
177 | let ports = self.bridge_ports.as_ref().unwrap_or(&EMPTY_LIST); |
178 | if ports.is_empty() { | |
179 | writeln!(w, "\tbridge-ports none")?; | |
180 | } else { | |
181 | writeln!(w, "\tbridge-ports {}", ports.join(" "))?; | |
1d9a68c2 DM |
182 | } |
183 | } | |
42fbe91a | 184 | NetworkInterfaceType::Bond => { |
bab5d18c DM |
185 | let mode = self.bond_mode.unwrap_or(LinuxBondMode::balance_rr); |
186 | writeln!(w, "\tbond-mode {}", bond_mode_to_str(mode))?; | |
85959a99 DC |
187 | if let Some(primary) = &self.bond_primary { |
188 | if mode == LinuxBondMode::active_backup { | |
189 | writeln!(w, "\tbond-primary {}", primary)?; | |
190 | } | |
191 | } | |
bab5d18c | 192 | |
8f2f3dd7 DC |
193 | if let Some(xmit_policy) = &self.bond_xmit_hash_policy { |
194 | if mode == LinuxBondMode::ieee802_3ad || | |
195 | mode == LinuxBondMode::balance_xor | |
196 | { | |
197 | writeln!(w, "\tbond_xmit_hash_policy {}", bond_xmit_hash_policy_to_str(xmit_policy))?; | |
198 | } | |
199 | } | |
200 | ||
db5672e8 DM |
201 | let slaves = self.slaves.as_ref().unwrap_or(&EMPTY_LIST); |
202 | if slaves.is_empty() { | |
203 | writeln!(w, "\tbond-slaves none")?; | |
204 | } else { | |
205 | writeln!(w, "\tbond-slaves {}", slaves.join(" "))?; | |
42fbe91a DM |
206 | } |
207 | } | |
1d9a68c2 DM |
208 | _ => {} |
209 | } | |
210 | ||
2c18efd9 | 211 | if let Some(mtu) = self.mtu { |
7b22acd0 | 212 | writeln!(w, "\tmtu {}", mtu)?; |
2c18efd9 | 213 | } |
1d9a68c2 | 214 | |
2c18efd9 DM |
215 | Ok(()) |
216 | } | |
217 | ||
add5861e | 218 | /// Write attributes depending on address family inet (IPv4) |
f8e7ac68 DM |
219 | fn write_iface_attributes_v4(&self, w: &mut dyn Write, method: NetworkConfigMethod) -> Result<(), Error> { |
220 | if method == NetworkConfigMethod::Static { | |
7b22acd0 DM |
221 | if let Some(address) = &self.cidr { |
222 | writeln!(w, "\taddress {}", address)?; | |
f8e7ac68 | 223 | } |
7b22acd0 DM |
224 | if let Some(gateway) = &self.gateway { |
225 | writeln!(w, "\tgateway {}", gateway)?; | |
f8e7ac68 | 226 | } |
a9bb491e | 227 | } |
f8e7ac68 | 228 | |
7b22acd0 DM |
229 | for option in &self.options { |
230 | writeln!(w, "\t{}", option)?; | |
a9bb491e DM |
231 | } |
232 | ||
7b22acd0 | 233 | if let Some(ref comments) = self.comments { |
8a6b86b8 DM |
234 | for comment in comments.lines() { |
235 | writeln!(w, "#{}", comment)?; | |
236 | } | |
5f60a58f DM |
237 | } |
238 | ||
a9bb491e DM |
239 | Ok(()) |
240 | } | |
241 | ||
add5861e | 242 | /// Write attributes depending on address family inet6 (IPv6) |
f8e7ac68 DM |
243 | fn write_iface_attributes_v6(&self, w: &mut dyn Write, method: NetworkConfigMethod) -> Result<(), Error> { |
244 | if method == NetworkConfigMethod::Static { | |
7b22acd0 DM |
245 | if let Some(address) = &self.cidr6 { |
246 | writeln!(w, "\taddress {}", address)?; | |
f8e7ac68 | 247 | } |
7b22acd0 DM |
248 | if let Some(gateway) = &self.gateway6 { |
249 | writeln!(w, "\tgateway {}", gateway)?; | |
f8e7ac68 | 250 | } |
a9bb491e | 251 | } |
f8e7ac68 | 252 | |
7b22acd0 DM |
253 | for option in &self.options6 { |
254 | writeln!(w, "\t{}", option)?; | |
a9bb491e DM |
255 | } |
256 | ||
7b22acd0 | 257 | if let Some(ref comments) = self.comments6 { |
8a6b86b8 DM |
258 | for comment in comments.lines() { |
259 | writeln!(w, "#{}", comment)?; | |
260 | } | |
5f60a58f DM |
261 | } |
262 | ||
a9bb491e DM |
263 | Ok(()) |
264 | } | |
265 | ||
266 | fn write_iface(&self, w: &mut dyn Write) -> Result<(), Error> { | |
267 | ||
7e02d08c | 268 | fn method_to_str(method: NetworkConfigMethod) -> &'static str { |
a9bb491e | 269 | match method { |
7e02d08c DM |
270 | NetworkConfigMethod::Static => "static", |
271 | NetworkConfigMethod::Loopback => "loopback", | |
272 | NetworkConfigMethod::Manual => "manual", | |
273 | NetworkConfigMethod::DHCP => "dhcp", | |
a9bb491e DM |
274 | } |
275 | } | |
276 | ||
7b22acd0 | 277 | if self.method.is_none() && self.method6.is_none() { return Ok(()); } |
f8e7ac68 | 278 | |
7b22acd0 | 279 | if self.autostart { |
a9bb491e DM |
280 | writeln!(w, "auto {}", self.name)?; |
281 | } | |
282 | ||
7b22acd0 DM |
283 | if let Some(method) = self.method { |
284 | writeln!(w, "iface {} inet {}", self.name, method_to_str(method))?; | |
285 | self.write_iface_attributes_v4(w, method)?; | |
286 | self.write_iface_attributes(w)?; | |
287 | writeln!(w)?; | |
288 | } | |
bab5d18c | 289 | |
7b22acd0 DM |
290 | if let Some(method6) = self.method6 { |
291 | let mut skip_v6 = false; // avoid empty inet6 manual entry | |
20813274 WB |
292 | if self.method.is_some() |
293 | && method6 == NetworkConfigMethod::Manual | |
294 | && self.comments6.is_none() | |
295 | && self.options6.is_empty() | |
296 | { | |
297 | skip_v6 = true; | |
a9bb491e | 298 | } |
bab5d18c | 299 | |
7b22acd0 DM |
300 | if !skip_v6 { |
301 | writeln!(w, "iface {} inet6 {}", self.name, method_to_str(method6))?; | |
302 | self.write_iface_attributes_v6(w, method6)?; | |
303 | if self.method.is_none() { // only write common attributes once | |
5f60a58f DM |
304 | self.write_iface_attributes(w)?; |
305 | } | |
a9bb491e DM |
306 | writeln!(w)?; |
307 | } | |
308 | } | |
7b22acd0 | 309 | |
a9bb491e DM |
310 | Ok(()) |
311 | } | |
f34d4401 DM |
312 | } |
313 | ||
314 | #[derive(Debug)] | |
315 | enum NetworkOrderEntry { | |
316 | Iface(String), | |
317 | Comment(String), | |
318 | Option(String), | |
319 | } | |
320 | ||
321 | #[derive(Debug)] | |
322 | pub struct NetworkConfig { | |
a4ccb461 | 323 | pub interfaces: BTreeMap<String, Interface>, |
f34d4401 DM |
324 | order: Vec<NetworkOrderEntry>, |
325 | } | |
326 | ||
1ca540a6 DM |
327 | use std::convert::TryFrom; |
328 | ||
329 | impl TryFrom<NetworkConfig> for String { | |
330 | ||
331 | type Error = Error; | |
332 | ||
333 | fn try_from(config: NetworkConfig) -> Result<Self, Self::Error> { | |
334 | let mut output = Vec::new(); | |
335 | config.write_config(&mut output)?; | |
336 | let res = String::from_utf8(output)?; | |
337 | Ok(res) | |
338 | } | |
339 | } | |
340 | ||
f34d4401 DM |
341 | impl NetworkConfig { |
342 | ||
343 | pub fn new() -> Self { | |
344 | Self { | |
a4ccb461 | 345 | interfaces: BTreeMap::new(), |
f34d4401 DM |
346 | order: Vec::new(), |
347 | } | |
348 | } | |
349 | ||
df6bb03d DM |
350 | pub fn lookup(&self, name: &str) -> Result<&Interface, Error> { |
351 | let interface = self.interfaces.get(name).ok_or_else(|| { | |
352 | format_err!("interface '{}' does not exist.", name) | |
353 | })?; | |
354 | Ok(interface) | |
355 | } | |
356 | ||
357 | pub fn lookup_mut(&mut self, name: &str) -> Result<&mut Interface, Error> { | |
358 | let interface = self.interfaces.get_mut(name).ok_or_else(|| { | |
359 | format_err!("interface '{}' does not exist.", name) | |
360 | })?; | |
361 | Ok(interface) | |
362 | } | |
363 | ||
a4ccb461 DM |
364 | /// Check if ports are used only once |
365 | pub fn check_port_usage(&self) -> Result<(), Error> { | |
366 | let mut used_ports = HashMap::new(); | |
367 | let mut check_port_usage = |iface, ports: &Vec<String>| { | |
368 | for port in ports.iter() { | |
369 | if let Some(prev_iface) = used_ports.get(port) { | |
370 | bail!("iface '{}' port '{}' is already used on interface '{}'", | |
371 | iface, port, prev_iface); | |
372 | } | |
373 | used_ports.insert(port.to_string(), iface); | |
374 | } | |
375 | Ok(()) | |
376 | }; | |
377 | ||
378 | for (iface, interface) in self.interfaces.iter() { | |
379 | if let Some(ports) = &interface.bridge_ports { check_port_usage(iface, ports)?; } | |
380 | if let Some(slaves) = &interface.slaves { check_port_usage(iface, slaves)?; } | |
381 | } | |
382 | Ok(()) | |
383 | } | |
384 | ||
0f6bdbb0 DM |
385 | /// Check if child mtu is less or equal than parent mtu |
386 | pub fn check_mtu(&self, parent_name: &str, child_name: &str) -> Result<(), Error> { | |
387 | ||
388 | let parent = self.interfaces.get(parent_name) | |
389 | .ok_or(format_err!("check_mtu - missing parent interface '{}'", parent_name))?; | |
390 | let child = self.interfaces.get(child_name) | |
391 | .ok_or(format_err!("check_mtu - missing child interface '{}'", child_name))?; | |
392 | ||
393 | let child_mtu = match child.mtu { | |
394 | Some(mtu) => mtu, | |
395 | None => return Ok(()), | |
396 | }; | |
397 | ||
398 | let parent_mtu = match parent.mtu { | |
399 | Some(mtu) => mtu, | |
400 | None => { | |
401 | if parent.interface_type == NetworkInterfaceType::Bond { | |
402 | child_mtu | |
403 | } else { | |
404 | 1500 | |
405 | } | |
406 | } | |
407 | }; | |
408 | ||
409 | if parent_mtu < child_mtu { | |
410 | bail!("interface '{}' - mtu {} is lower than '{}' - mtu {}\n", | |
411 | parent_name, parent_mtu, child_name, child_mtu); | |
412 | } | |
413 | ||
414 | Ok(()) | |
415 | } | |
416 | ||
417 | /// Check if bond slaves exists | |
418 | pub fn check_bond_slaves(&self) -> Result<(), Error> { | |
419 | for (iface, interface) in self.interfaces.iter() { | |
420 | if let Some(slaves) = &interface.slaves { | |
421 | for slave in slaves.iter() { | |
422 | match self.interfaces.get(slave) { | |
423 | Some(entry) => { | |
424 | if entry.interface_type != NetworkInterfaceType::Eth { | |
425 | bail!("bond '{}' - wrong interface type on slave '{}' ({:?} != {:?})", | |
426 | iface, slave, entry.interface_type, NetworkInterfaceType::Eth); | |
427 | } | |
428 | } | |
429 | None => { | |
430 | bail!("bond '{}' - unable to find slave '{}'", iface, slave); | |
431 | } | |
432 | } | |
433 | self.check_mtu(iface, slave)?; | |
434 | } | |
435 | } | |
436 | } | |
437 | Ok(()) | |
438 | } | |
439 | ||
440 | /// Check if bridge ports exists | |
441 | pub fn check_bridge_ports(&self) -> Result<(), Error> { | |
5bef0f43 DM |
442 | lazy_static!{ |
443 | static ref VLAN_INTERFACE_REGEX: Regex = Regex::new(r"^(\S+)\.(\d+)$").unwrap(); | |
444 | } | |
445 | ||
0f6bdbb0 DM |
446 | for (iface, interface) in self.interfaces.iter() { |
447 | if let Some(ports) = &interface.bridge_ports { | |
448 | for port in ports.iter() { | |
5bef0f43 DM |
449 | let captures = VLAN_INTERFACE_REGEX.captures(port); |
450 | let port = if let Some(ref caps) = captures { &caps[1] } else { port.as_str() }; | |
0f6bdbb0 DM |
451 | if !self.interfaces.contains_key(port) { |
452 | bail!("bridge '{}' - unable to find port '{}'", iface, port); | |
453 | } | |
454 | self.check_mtu(iface, port)?; | |
455 | } | |
456 | } | |
457 | } | |
458 | Ok(()) | |
459 | } | |
460 | ||
f34d4401 DM |
461 | pub fn write_config(&self, w: &mut dyn Write) -> Result<(), Error> { |
462 | ||
a4ccb461 | 463 | self.check_port_usage()?; |
0f6bdbb0 DM |
464 | self.check_bond_slaves()?; |
465 | self.check_bridge_ports()?; | |
a4ccb461 | 466 | |
f34d4401 DM |
467 | let mut done = HashSet::new(); |
468 | ||
469 | let mut last_entry_was_comment = false; | |
470 | ||
471 | for entry in self.order.iter() { | |
a9bb491e | 472 | match entry { |
f34d4401 DM |
473 | NetworkOrderEntry::Comment(comment) => { |
474 | writeln!(w, "#{}", comment)?; | |
475 | last_entry_was_comment = true; | |
476 | } | |
477 | NetworkOrderEntry::Option(option) => { | |
478 | if last_entry_was_comment { writeln!(w)?; } | |
479 | last_entry_was_comment = false; | |
480 | writeln!(w, "{}", option)?; | |
481 | writeln!(w)?; | |
482 | } | |
483 | NetworkOrderEntry::Iface(name) => { | |
484 | let interface = match self.interfaces.get(name) { | |
485 | Some(interface) => interface, | |
486 | None => continue, | |
487 | }; | |
488 | ||
489 | if last_entry_was_comment { writeln!(w)?; } | |
490 | last_entry_was_comment = false; | |
491 | ||
492 | if done.contains(name) { continue; } | |
493 | done.insert(name); | |
494 | ||
a9bb491e | 495 | interface.write_iface(w)?; |
f34d4401 DM |
496 | } |
497 | } | |
498 | } | |
499 | ||
500 | for (name, interface) in &self.interfaces { | |
501 | if done.contains(name) { continue; } | |
a9bb491e | 502 | interface.write_iface(w)?; |
f34d4401 DM |
503 | } |
504 | Ok(()) | |
505 | } | |
506 | } | |
904e9886 DM |
507 | |
508 | pub const NETWORK_INTERFACES_FILENAME: &str = "/etc/network/interfaces"; | |
02e36d96 | 509 | pub const NETWORK_INTERFACES_NEW_FILENAME: &str = "/etc/network/interfaces.new"; |
904e9886 DM |
510 | pub const NETWORK_LOCKFILE: &str = "/var/lock/pve-network.lck"; |
511 | ||
512 | pub fn config() -> Result<(NetworkConfig, [u8;32]), Error> { | |
02e36d96 | 513 | |
3eeba687 DM |
514 | let content = match proxmox::tools::fs::file_get_optional_contents(NETWORK_INTERFACES_NEW_FILENAME)? { |
515 | Some(content) => content, | |
516 | None => { | |
517 | let content = proxmox::tools::fs::file_get_optional_contents(NETWORK_INTERFACES_FILENAME)?; | |
518 | content.unwrap_or(Vec::new()) | |
519 | } | |
520 | }; | |
904e9886 DM |
521 | |
522 | let digest = openssl::sha::sha256(&content); | |
523 | ||
1ca540a6 | 524 | let existing_interfaces = get_network_interfaces()?; |
904e9886 | 525 | let mut parser = NetworkParser::new(&content[..]); |
1ca540a6 | 526 | let data = parser.parse_interfaces(Some(&existing_interfaces))?; |
904e9886 DM |
527 | |
528 | Ok((data, digest)) | |
529 | } | |
530 | ||
2eefd9ae DM |
531 | pub fn changes() -> Result<String, Error> { |
532 | ||
533 | if !std::path::Path::new(NETWORK_INTERFACES_NEW_FILENAME).exists() { | |
534 | return Ok(String::new()); | |
535 | } | |
536 | ||
537 | compute_file_diff(NETWORK_INTERFACES_FILENAME, NETWORK_INTERFACES_NEW_FILENAME) | |
538 | } | |
539 | ||
904e9886 DM |
540 | pub fn save_config(config: &NetworkConfig) -> Result<(), Error> { |
541 | ||
542 | let mut raw = Vec::new(); | |
543 | config.write_config(&mut raw)?; | |
544 | ||
545 | let mode = nix::sys::stat::Mode::from_bits_truncate(0o0644); | |
546 | // set the correct owner/group/permissions while saving file | |
547 | // owner(rw) = root, group(r)=root, others(r) | |
548 | let options = CreateOptions::new() | |
549 | .perm(mode) | |
550 | .owner(nix::unistd::ROOT) | |
551 | .group(nix::unistd::Gid::from_raw(0)); | |
552 | ||
02e36d96 | 553 | replace_file(NETWORK_INTERFACES_NEW_FILENAME, &raw, options)?; |
904e9886 DM |
554 | |
555 | Ok(()) | |
556 | } | |
df6bb03d DM |
557 | |
558 | // shell completion helper | |
559 | pub fn complete_interface_name(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> { | |
560 | match config() { | |
561 | Ok((data, _digest)) => data.interfaces.keys().map(|id| id.to_string()).collect(), | |
562 | Err(_) => return vec![], | |
563 | } | |
564 | } | |
1ca540a6 | 565 | |
65dab026 DM |
566 | |
567 | pub fn complete_port_list(arg: &str, _param: &HashMap<String, String>) -> Vec<String> { | |
568 | let mut ports = Vec::new(); | |
569 | match config() { | |
570 | Ok((data, _digest)) => { | |
571 | for (iface, interface) in data.interfaces.iter() { | |
572 | if interface.interface_type == NetworkInterfaceType::Eth { | |
573 | ports.push(iface.to_string()); | |
574 | } | |
575 | } | |
576 | } | |
577 | Err(_) => return vec![], | |
578 | }; | |
579 | ||
44288184 | 580 | let arg = arg.trim(); |
d8d8af98 | 581 | let prefix = if let Some(idx) = arg.rfind(',') { &arg[..idx+1] } else { "" }; |
65dab026 DM |
582 | ports.iter().map(|port| format!("{}{}", prefix, port)).collect() |
583 | } | |
584 | ||
1ca540a6 DM |
585 | #[cfg(test)] |
586 | mod test { | |
587 | ||
588 | use anyhow::{Error}; | |
589 | ||
590 | use super::*; | |
591 | ||
592 | #[test] | |
593 | fn test_network_config_create_lo_1() -> Result<(), Error> { | |
594 | ||
595 | let input = ""; | |
596 | ||
597 | let mut parser = NetworkParser::new(&input.as_bytes()[..]); | |
598 | ||
599 | let config = parser.parse_interfaces(None)?; | |
600 | ||
601 | let output = String::try_from(config)?; | |
602 | ||
603 | let expected = "auto lo\niface lo inet loopback\n\n"; | |
604 | assert_eq!(output, expected); | |
605 | ||
606 | // run again using output as input | |
607 | let mut parser = NetworkParser::new(&output.as_bytes()[..]); | |
608 | ||
609 | let config = parser.parse_interfaces(None)?; | |
610 | ||
611 | let output = String::try_from(config)?; | |
612 | ||
613 | assert_eq!(output, expected); | |
614 | ||
615 | Ok(()) | |
616 | } | |
617 | ||
618 | #[test] | |
619 | fn test_network_config_create_lo_2() -> Result<(), Error> { | |
620 | ||
621 | let input = "#c1\n\n#c2\n\niface test inet manual\n"; | |
622 | ||
623 | let mut parser = NetworkParser::new(&input.as_bytes()[..]); | |
624 | ||
625 | let config = parser.parse_interfaces(None)?; | |
626 | ||
627 | let output = String::try_from(config)?; | |
628 | ||
629 | // Note: loopback should be added in front of other interfaces | |
630 | let expected = "#c1\n#c2\n\nauto lo\niface lo inet loopback\n\niface test inet manual\n\n"; | |
631 | assert_eq!(output, expected); | |
632 | ||
633 | Ok(()) | |
634 | } | |
87c4cb74 FE |
635 | |
636 | #[test] | |
637 | fn test_network_config_parser_no_blank_1() -> Result<(), Error> { | |
638 | let input = "auto lo\n\ | |
639 | iface lo inet loopback\n\ | |
640 | iface lo inet6 loopback\n\ | |
641 | auto ens18\n\ | |
642 | iface ens18 inet static\n\ | |
643 | \taddress 192.168.20.144/20\n\ | |
644 | \tgateway 192.168.16.1\n\ | |
645 | # comment\n\ | |
646 | iface ens20 inet static\n\ | |
647 | \taddress 192.168.20.145/20\n\ | |
648 | iface ens21 inet manual\n\ | |
649 | iface ens22 inet manual\n"; | |
650 | ||
651 | let mut parser = NetworkParser::new(&input.as_bytes()[..]); | |
652 | ||
653 | let config = parser.parse_interfaces(None)?; | |
654 | ||
655 | let output = String::try_from(config)?; | |
656 | ||
657 | let expected = "auto lo\n\ | |
658 | iface lo inet loopback\n\ | |
659 | \n\ | |
660 | iface lo inet6 loopback\n\ | |
661 | \n\ | |
662 | auto ens18\n\ | |
663 | iface ens18 inet static\n\ | |
664 | \taddress 192.168.20.144/20\n\ | |
665 | \tgateway 192.168.16.1\n\ | |
666 | #comment\n\ | |
667 | \n\ | |
668 | iface ens20 inet static\n\ | |
669 | \taddress 192.168.20.145/20\n\ | |
670 | \n\ | |
671 | iface ens21 inet manual\n\ | |
672 | \n\ | |
673 | iface ens22 inet manual\n\ | |
674 | \n"; | |
675 | assert_eq!(output, expected); | |
676 | ||
677 | Ok(()) | |
678 | } | |
679 | ||
680 | #[test] | |
681 | fn test_network_config_parser_no_blank_2() -> Result<(), Error> { | |
682 | // Adapted from bug 2926 | |
683 | let input = "### Hetzner Online GmbH installimage\n\ | |
684 | \n\ | |
685 | source /etc/network/interfaces.d/*\n\ | |
686 | \n\ | |
687 | auto lo\n\ | |
688 | iface lo inet loopback\n\ | |
689 | iface lo inet6 loopback\n\ | |
690 | \n\ | |
691 | auto enp4s0\n\ | |
692 | iface enp4s0 inet static\n\ | |
693 | \taddress 10.10.10.10/24\n\ | |
694 | \tgateway 10.10.10.1\n\ | |
695 | \t# route 10.10.20.10/24 via 10.10.20.1\n\ | |
696 | \tup route add -net 10.10.20.10 netmask 255.255.255.0 gw 10.10.20.1 dev enp4s0\n\ | |
697 | \n\ | |
698 | iface enp4s0 inet6 static\n\ | |
699 | \taddress fe80::5496:35ff:fe99:5a6a/64\n\ | |
700 | \tgateway fe80::1\n"; | |
701 | ||
702 | let mut parser = NetworkParser::new(&input.as_bytes()[..]); | |
703 | ||
704 | let config = parser.parse_interfaces(None)?; | |
705 | ||
706 | let output = String::try_from(config)?; | |
707 | ||
708 | let expected = "### Hetzner Online GmbH installimage\n\ | |
709 | \n\ | |
710 | source /etc/network/interfaces.d/*\n\ | |
711 | \n\ | |
712 | auto lo\n\ | |
713 | iface lo inet loopback\n\ | |
714 | \n\ | |
715 | iface lo inet6 loopback\n\ | |
716 | \n\ | |
717 | auto enp4s0\n\ | |
718 | iface enp4s0 inet static\n\ | |
719 | \taddress 10.10.10.10/24\n\ | |
720 | \tgateway 10.10.10.1\n\ | |
721 | \t# route 10.10.20.10/24 via 10.10.20.1\n\ | |
722 | \tup route add -net 10.10.20.10 netmask 255.255.255.0 gw 10.10.20.1 dev enp4s0\n\ | |
723 | \n\ | |
724 | iface enp4s0 inet6 static\n\ | |
725 | \taddress fe80::5496:35ff:fe99:5a6a/64\n\ | |
726 | \tgateway fe80::1\n\ | |
727 | \n"; | |
728 | assert_eq!(output, expected); | |
729 | ||
730 | Ok(()) | |
731 | } | |
1ca540a6 | 732 | } |