]> git.proxmox.com Git - proxmox-backup.git/blame_incremental - src/api2/node/network.rs
move required_X_param to pbs_tools::json
[proxmox-backup.git] / src / api2 / node / network.rs
... / ...
CommitLineData
1use anyhow::{Error, bail};
2use serde_json::{Value, to_value};
3use ::serde::{Deserialize, Serialize};
4
5use proxmox::api::{api, ApiMethod, Router, RpcEnvironment, Permission};
6use proxmox::api::schema::parse_property_string;
7use proxmox::tools::fs::open_file_locked;
8
9use crate::config::network::{self, NetworkConfig};
10use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY};
11use crate::api2::types::*;
12use crate::server::{WorkerTask};
13
14fn split_interface_list(list: &str) -> Result<Vec<String>, Error> {
15 let value = parse_property_string(&list, &NETWORK_INTERFACE_ARRAY_SCHEMA)?;
16 Ok(value.as_array().unwrap().iter().map(|v| v.as_str().unwrap().to_string()).collect())
17}
18
19fn check_duplicate_gateway_v4(config: &NetworkConfig, iface: &str) -> Result<(), Error> {
20
21 let current_gateway_v4 = config.interfaces.iter()
22 .find(|(_, interface)| interface.gateway.is_some())
23 .map(|(name, _)| name.to_string());
24
25 if let Some(current_gateway_v4) = current_gateway_v4 {
26 if current_gateway_v4 != iface {
27 bail!("Default IPv4 gateway already exists on interface '{}'", current_gateway_v4);
28 }
29 }
30 Ok(())
31}
32
33fn check_duplicate_gateway_v6(config: &NetworkConfig, iface: &str) -> Result<(), Error> {
34
35 let current_gateway_v6 = config.interfaces.iter()
36 .find(|(_, interface)| interface.gateway6.is_some())
37 .map(|(name, _)| name.to_string());
38
39 if let Some(current_gateway_v6) = current_gateway_v6 {
40 if current_gateway_v6 != iface {
41 bail!("Default IPv6 gateway already exists on interface '{}'", current_gateway_v6);
42 }
43 }
44 Ok(())
45}
46
47#[api(
48 input: {
49 properties: {
50 node: {
51 schema: NODE_SCHEMA,
52 },
53 },
54 },
55 returns: {
56 description: "List network devices (with config digest).",
57 type: Array,
58 items: {
59 type: Interface,
60 },
61 },
62 access: {
63 permission: &Permission::Privilege(&["system", "network", "interfaces"], PRIV_SYS_AUDIT, false),
64 },
65)]
66/// List all datastores
67pub fn list_network_devices(
68 _param: Value,
69 _info: &ApiMethod,
70 mut rpcenv: &mut dyn RpcEnvironment,
71) -> Result<Value, Error> {
72
73 let (config, digest) = network::config()?;
74 let digest = proxmox::tools::digest_to_hex(&digest);
75
76 let mut list = Vec::new();
77
78 for (iface, interface) in config.interfaces.iter() {
79 if iface == "lo" { continue; } // do not list lo
80 let mut item: Value = to_value(interface)?;
81 item["digest"] = digest.clone().into();
82 item["iface"] = iface.to_string().into();
83 list.push(item);
84 }
85
86 let diff = network::changes()?;
87 if !diff.is_empty() {
88 rpcenv["changes"] = diff.into();
89 }
90
91 Ok(list.into())
92}
93
94#[api(
95 input: {
96 properties: {
97 node: {
98 schema: NODE_SCHEMA,
99 },
100 iface: {
101 schema: NETWORK_INTERFACE_NAME_SCHEMA,
102 },
103 },
104 },
105 returns: { type: Interface },
106 access: {
107 permission: &Permission::Privilege(&["system", "network", "interfaces", "{name}"], PRIV_SYS_AUDIT, false),
108 },
109)]
110/// Read a network interface configuration.
111pub fn read_interface(iface: String) -> Result<Value, Error> {
112
113 let (config, digest) = network::config()?;
114
115 let interface = config.lookup(&iface)?;
116
117 let mut data: Value = to_value(interface)?;
118 data["digest"] = proxmox::tools::digest_to_hex(&digest).into();
119
120 Ok(data)
121}
122
123
124#[api(
125 protected: true,
126 input: {
127 properties: {
128 node: {
129 schema: NODE_SCHEMA,
130 },
131 iface: {
132 schema: NETWORK_INTERFACE_NAME_SCHEMA,
133 },
134 "type": {
135 type: NetworkInterfaceType,
136 optional: true,
137 },
138 autostart: {
139 description: "Autostart interface.",
140 type: bool,
141 optional: true,
142 },
143 method: {
144 type: NetworkConfigMethod,
145 optional: true,
146 },
147 method6: {
148 type: NetworkConfigMethod,
149 optional: true,
150 },
151 comments: {
152 description: "Comments (inet, may span multiple lines)",
153 type: String,
154 optional: true,
155 },
156 comments6: {
157 description: "Comments (inet5, may span multiple lines)",
158 type: String,
159 optional: true,
160 },
161 cidr: {
162 schema: CIDR_V4_SCHEMA,
163 optional: true,
164 },
165 cidr6: {
166 schema: CIDR_V6_SCHEMA,
167 optional: true,
168 },
169 gateway: {
170 schema: IP_V4_SCHEMA,
171 optional: true,
172 },
173 gateway6: {
174 schema: IP_V6_SCHEMA,
175 optional: true,
176 },
177 mtu: {
178 description: "Maximum Transmission Unit.",
179 optional: true,
180 minimum: 46,
181 maximum: 65535,
182 default: 1500,
183 },
184 bridge_ports: {
185 schema: NETWORK_INTERFACE_LIST_SCHEMA,
186 optional: true,
187 },
188 bridge_vlan_aware: {
189 description: "Enable bridge vlan support.",
190 type: bool,
191 optional: true,
192 },
193 bond_mode: {
194 type: LinuxBondMode,
195 optional: true,
196 },
197 "bond-primary": {
198 schema: NETWORK_INTERFACE_NAME_SCHEMA,
199 optional: true,
200 },
201 bond_xmit_hash_policy: {
202 type: BondXmitHashPolicy,
203 optional: true,
204 },
205 slaves: {
206 schema: NETWORK_INTERFACE_LIST_SCHEMA,
207 optional: true,
208 },
209 },
210 },
211 access: {
212 permission: &Permission::Privilege(&["system", "network", "interfaces", "{iface}"], PRIV_SYS_MODIFY, false),
213 },
214)]
215/// Create network interface configuration.
216#[allow(clippy::too_many_arguments)]
217pub fn create_interface(
218 iface: String,
219 autostart: Option<bool>,
220 method: Option<NetworkConfigMethod>,
221 method6: Option<NetworkConfigMethod>,
222 comments: Option<String>,
223 comments6: Option<String>,
224 cidr: Option<String>,
225 gateway: Option<String>,
226 cidr6: Option<String>,
227 gateway6: Option<String>,
228 mtu: Option<u64>,
229 bridge_ports: Option<String>,
230 bridge_vlan_aware: Option<bool>,
231 bond_mode: Option<LinuxBondMode>,
232 bond_primary: Option<String>,
233 bond_xmit_hash_policy: Option<BondXmitHashPolicy>,
234 slaves: Option<String>,
235 param: Value,
236) -> Result<(), Error> {
237
238 let interface_type = pbs_tools::json::required_string_param(&param, "type")?;
239 let interface_type: NetworkInterfaceType = serde_json::from_value(interface_type.into())?;
240
241 let _lock = open_file_locked(network::NETWORK_LOCKFILE, std::time::Duration::new(10, 0), true)?;
242
243 let (mut config, _digest) = network::config()?;
244
245 if config.interfaces.contains_key(&iface) {
246 bail!("interface '{}' already exists", iface);
247 }
248
249 let mut interface = Interface::new(iface.clone());
250 interface.interface_type = interface_type;
251
252 if let Some(autostart) = autostart { interface.autostart = autostart; }
253 if method.is_some() { interface.method = method; }
254 if method6.is_some() { interface.method6 = method6; }
255 if mtu.is_some() { interface.mtu = mtu; }
256 if comments.is_some() { interface.comments = comments; }
257 if comments6.is_some() { interface.comments6 = comments6; }
258
259 if let Some(cidr) = cidr {
260 let (_, _, is_v6) = network::parse_cidr(&cidr)?;
261 if is_v6 { bail!("invalid address type (expected IPv4, got IPv6)"); }
262 interface.cidr = Some(cidr);
263 }
264
265 if let Some(cidr6) = cidr6 {
266 let (_, _, is_v6) = network::parse_cidr(&cidr6)?;
267 if !is_v6 { bail!("invalid address type (expected IPv6, got IPv4)"); }
268 interface.cidr6 = Some(cidr6);
269 }
270
271 if let Some(gateway) = gateway {
272 let is_v6 = gateway.contains(':');
273 if is_v6 { bail!("invalid address type (expected IPv4, got IPv6)"); }
274 check_duplicate_gateway_v4(&config, &iface)?;
275 interface.gateway = Some(gateway);
276 }
277
278 if let Some(gateway6) = gateway6 {
279 let is_v6 = gateway6.contains(':');
280 if !is_v6 { bail!("invalid address type (expected IPv6, got IPv4)"); }
281 check_duplicate_gateway_v6(&config, &iface)?;
282 interface.gateway6 = Some(gateway6);
283 }
284
285 match interface_type {
286 NetworkInterfaceType::Bridge => {
287 if let Some(ports) = bridge_ports {
288 let ports = split_interface_list(&ports)?;
289 interface.set_bridge_ports(ports)?;
290 }
291 if bridge_vlan_aware.is_some() { interface.bridge_vlan_aware = bridge_vlan_aware; }
292 }
293 NetworkInterfaceType::Bond => {
294 if let Some(mode) = bond_mode {
295 interface.bond_mode = bond_mode;
296 if bond_primary.is_some() {
297 if mode != LinuxBondMode::active_backup {
298 bail!("bond-primary is only valid with Active/Backup mode");
299 }
300 interface.bond_primary = bond_primary;
301 }
302 if bond_xmit_hash_policy.is_some() {
303 if mode != LinuxBondMode::ieee802_3ad &&
304 mode != LinuxBondMode::balance_xor
305 {
306 bail!("bond_xmit_hash_policy is only valid with LACP(802.3ad) or balance-xor mode");
307 }
308 interface.bond_xmit_hash_policy = bond_xmit_hash_policy;
309 }
310 }
311 if let Some(slaves) = slaves {
312 let slaves = split_interface_list(&slaves)?;
313 interface.set_bond_slaves(slaves)?;
314 }
315 }
316 _ => bail!("creating network interface type '{:?}' is not supported", interface_type),
317 }
318
319 if interface.cidr.is_some() || interface.gateway.is_some() {
320 interface.method = Some(NetworkConfigMethod::Static);
321 } else if interface.method.is_none() {
322 interface.method = Some(NetworkConfigMethod::Manual);
323 }
324
325 if interface.cidr6.is_some() || interface.gateway6.is_some() {
326 interface.method6 = Some(NetworkConfigMethod::Static);
327 } else if interface.method6.is_none() {
328 interface.method6 = Some(NetworkConfigMethod::Manual);
329 }
330
331 config.interfaces.insert(iface, interface);
332
333 network::save_config(&config)?;
334
335 Ok(())
336}
337
338#[api()]
339#[derive(Serialize, Deserialize)]
340#[allow(non_camel_case_types)]
341/// Deletable property name
342pub enum DeletableProperty {
343 /// Delete the IPv4 address property.
344 cidr,
345 /// Delete the IPv6 address property.
346 cidr6,
347 /// Delete the IPv4 gateway property.
348 gateway,
349 /// Delete the IPv6 gateway property.
350 gateway6,
351 /// Delete the whole IPv4 configuration entry.
352 method,
353 /// Delete the whole IPv6 configuration entry.
354 method6,
355 /// Delete IPv4 comments
356 comments,
357 /// Delete IPv6 comments
358 comments6,
359 /// Delete mtu.
360 mtu,
361 /// Delete autostart flag
362 autostart,
363 /// Delete bridge ports (set to 'none')
364 bridge_ports,
365 /// Delete bridge-vlan-aware flag
366 bridge_vlan_aware,
367 /// Delete bond-slaves (set to 'none')
368 slaves,
369 /// Delete bond-primary
370 #[serde(rename = "bond-primary")]
371 bond_primary,
372 /// Delete bond transmit hash policy
373 bond_xmit_hash_policy,
374}
375
376
377#[api(
378 protected: true,
379 input: {
380 properties: {
381 node: {
382 schema: NODE_SCHEMA,
383 },
384 iface: {
385 schema: NETWORK_INTERFACE_NAME_SCHEMA,
386 },
387 "type": {
388 type: NetworkInterfaceType,
389 optional: true,
390 },
391 autostart: {
392 description: "Autostart interface.",
393 type: bool,
394 optional: true,
395 },
396 method: {
397 type: NetworkConfigMethod,
398 optional: true,
399 },
400 method6: {
401 type: NetworkConfigMethod,
402 optional: true,
403 },
404 comments: {
405 description: "Comments (inet, may span multiple lines)",
406 type: String,
407 optional: true,
408 },
409 comments6: {
410 description: "Comments (inet5, may span multiple lines)",
411 type: String,
412 optional: true,
413 },
414 cidr: {
415 schema: CIDR_V4_SCHEMA,
416 optional: true,
417 },
418 cidr6: {
419 schema: CIDR_V6_SCHEMA,
420 optional: true,
421 },
422 gateway: {
423 schema: IP_V4_SCHEMA,
424 optional: true,
425 },
426 gateway6: {
427 schema: IP_V6_SCHEMA,
428 optional: true,
429 },
430 mtu: {
431 description: "Maximum Transmission Unit.",
432 optional: true,
433 minimum: 46,
434 maximum: 65535,
435 default: 1500,
436 },
437 bridge_ports: {
438 schema: NETWORK_INTERFACE_LIST_SCHEMA,
439 optional: true,
440 },
441 bridge_vlan_aware: {
442 description: "Enable bridge vlan support.",
443 type: bool,
444 optional: true,
445 },
446 bond_mode: {
447 type: LinuxBondMode,
448 optional: true,
449 },
450 "bond-primary": {
451 schema: NETWORK_INTERFACE_NAME_SCHEMA,
452 optional: true,
453 },
454 bond_xmit_hash_policy: {
455 type: BondXmitHashPolicy,
456 optional: true,
457 },
458 slaves: {
459 schema: NETWORK_INTERFACE_LIST_SCHEMA,
460 optional: true,
461 },
462 delete: {
463 description: "List of properties to delete.",
464 type: Array,
465 optional: true,
466 items: {
467 type: DeletableProperty,
468 }
469 },
470 digest: {
471 optional: true,
472 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
473 },
474 },
475 },
476 access: {
477 permission: &Permission::Privilege(&["system", "network", "interfaces", "{iface}"], PRIV_SYS_MODIFY, false),
478 },
479)]
480/// Update network interface config.
481#[allow(clippy::too_many_arguments)]
482pub fn update_interface(
483 iface: String,
484 autostart: Option<bool>,
485 method: Option<NetworkConfigMethod>,
486 method6: Option<NetworkConfigMethod>,
487 comments: Option<String>,
488 comments6: Option<String>,
489 cidr: Option<String>,
490 gateway: Option<String>,
491 cidr6: Option<String>,
492 gateway6: Option<String>,
493 mtu: Option<u64>,
494 bridge_ports: Option<String>,
495 bridge_vlan_aware: Option<bool>,
496 bond_mode: Option<LinuxBondMode>,
497 bond_primary: Option<String>,
498 bond_xmit_hash_policy: Option<BondXmitHashPolicy>,
499 slaves: Option<String>,
500 delete: Option<Vec<DeletableProperty>>,
501 digest: Option<String>,
502 param: Value,
503) -> Result<(), Error> {
504
505 let _lock = open_file_locked(network::NETWORK_LOCKFILE, std::time::Duration::new(10, 0), true)?;
506
507 let (mut config, expected_digest) = network::config()?;
508
509 if let Some(ref digest) = digest {
510 let digest = proxmox::tools::hex_to_digest(digest)?;
511 crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
512 }
513
514 if gateway.is_some() { check_duplicate_gateway_v4(&config, &iface)?; }
515 if gateway6.is_some() { check_duplicate_gateway_v6(&config, &iface)?; }
516
517 let interface = config.lookup_mut(&iface)?;
518
519 if let Some(interface_type) = param.get("type") {
520 let interface_type: NetworkInterfaceType = serde_json::from_value(interface_type.clone())?;
521 if interface_type != interface.interface_type {
522 bail!("got unexpected interface type ({:?} != {:?})", interface_type, interface.interface_type);
523 }
524 }
525
526 if let Some(delete) = delete {
527 for delete_prop in delete {
528 match delete_prop {
529 DeletableProperty::cidr => { interface.cidr = None; },
530 DeletableProperty::cidr6 => { interface.cidr6 = None; },
531 DeletableProperty::gateway => { interface.gateway = None; },
532 DeletableProperty::gateway6 => { interface.gateway6 = None; },
533 DeletableProperty::method => { interface.method = None; },
534 DeletableProperty::method6 => { interface.method6 = None; },
535 DeletableProperty::comments => { interface.comments = None; },
536 DeletableProperty::comments6 => { interface.comments6 = None; },
537 DeletableProperty::mtu => { interface.mtu = None; },
538 DeletableProperty::autostart => { interface.autostart = false; },
539 DeletableProperty::bridge_ports => { interface.set_bridge_ports(Vec::new())?; }
540 DeletableProperty::bridge_vlan_aware => { interface.bridge_vlan_aware = None; }
541 DeletableProperty::slaves => { interface.set_bond_slaves(Vec::new())?; }
542 DeletableProperty::bond_primary => { interface.bond_primary = None; }
543 DeletableProperty::bond_xmit_hash_policy => { interface.bond_xmit_hash_policy = None }
544 }
545 }
546 }
547
548 if let Some(autostart) = autostart { interface.autostart = autostart; }
549 if method.is_some() { interface.method = method; }
550 if method6.is_some() { interface.method6 = method6; }
551 if mtu.is_some() { interface.mtu = mtu; }
552 if let Some(ports) = bridge_ports {
553 let ports = split_interface_list(&ports)?;
554 interface.set_bridge_ports(ports)?;
555 }
556 if bridge_vlan_aware.is_some() { interface.bridge_vlan_aware = bridge_vlan_aware; }
557 if let Some(slaves) = slaves {
558 let slaves = split_interface_list(&slaves)?;
559 interface.set_bond_slaves(slaves)?;
560 }
561 if let Some(mode) = bond_mode {
562 interface.bond_mode = bond_mode;
563 if bond_primary.is_some() {
564 if mode != LinuxBondMode::active_backup {
565 bail!("bond-primary is only valid with Active/Backup mode");
566 }
567 interface.bond_primary = bond_primary;
568 }
569 if bond_xmit_hash_policy.is_some() {
570 if mode != LinuxBondMode::ieee802_3ad &&
571 mode != LinuxBondMode::balance_xor
572 {
573 bail!("bond_xmit_hash_policy is only valid with LACP(802.3ad) or balance-xor mode");
574 }
575 interface.bond_xmit_hash_policy = bond_xmit_hash_policy;
576 }
577 }
578
579 if let Some(cidr) = cidr {
580 let (_, _, is_v6) = network::parse_cidr(&cidr)?;
581 if is_v6 { bail!("invalid address type (expected IPv4, got IPv6)"); }
582 interface.cidr = Some(cidr);
583 }
584
585 if let Some(cidr6) = cidr6 {
586 let (_, _, is_v6) = network::parse_cidr(&cidr6)?;
587 if !is_v6 { bail!("invalid address type (expected IPv6, got IPv4)"); }
588 interface.cidr6 = Some(cidr6);
589 }
590
591 if let Some(gateway) = gateway {
592 let is_v6 = gateway.contains(':');
593 if is_v6 { bail!("invalid address type (expected IPv4, got IPv6)"); }
594 interface.gateway = Some(gateway);
595 }
596
597 if let Some(gateway6) = gateway6 {
598 let is_v6 = gateway6.contains(':');
599 if !is_v6 { bail!("invalid address type (expected IPv6, got IPv4)"); }
600 interface.gateway6 = Some(gateway6);
601 }
602
603 if comments.is_some() { interface.comments = comments; }
604 if comments6.is_some() { interface.comments6 = comments6; }
605
606 if interface.cidr.is_some() || interface.gateway.is_some() {
607 interface.method = Some(NetworkConfigMethod::Static);
608 } else {
609 interface.method = Some(NetworkConfigMethod::Manual);
610 }
611
612 if interface.cidr6.is_some() || interface.gateway6.is_some() {
613 interface.method6 = Some(NetworkConfigMethod::Static);
614 } else {
615 interface.method6 = Some(NetworkConfigMethod::Manual);
616 }
617
618 network::save_config(&config)?;
619
620 Ok(())
621}
622
623#[api(
624 protected: true,
625 input: {
626 properties: {
627 node: {
628 schema: NODE_SCHEMA,
629 },
630 iface: {
631 schema: NETWORK_INTERFACE_NAME_SCHEMA,
632 },
633 digest: {
634 optional: true,
635 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
636 },
637 },
638 },
639 access: {
640 permission: &Permission::Privilege(&["system", "network", "interfaces", "{iface}"], PRIV_SYS_MODIFY, false),
641 },
642)]
643/// Remove network interface configuration.
644pub fn delete_interface(iface: String, digest: Option<String>) -> Result<(), Error> {
645
646 let _lock = open_file_locked(network::NETWORK_LOCKFILE, std::time::Duration::new(10, 0), true)?;
647
648 let (mut config, expected_digest) = network::config()?;
649
650 if let Some(ref digest) = digest {
651 let digest = proxmox::tools::hex_to_digest(digest)?;
652 crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
653 }
654
655 let _interface = config.lookup(&iface)?; // check if interface exists
656
657 config.interfaces.remove(&iface);
658
659 network::save_config(&config)?;
660
661 Ok(())
662}
663
664#[api(
665 protected: true,
666 input: {
667 properties: {
668 node: {
669 schema: NODE_SCHEMA,
670 },
671 },
672 },
673 access: {
674 permission: &Permission::Privilege(&["system", "network", "interfaces"], PRIV_SYS_MODIFY, false),
675 },
676)]
677/// Reload network configuration (requires ifupdown2).
678pub async fn reload_network_config(
679 rpcenv: &mut dyn RpcEnvironment,
680) -> Result<String, Error> {
681
682 network::assert_ifupdown2_installed()?;
683
684 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
685
686 let upid_str = WorkerTask::spawn("srvreload", Some(String::from("networking")), auth_id, true, |_worker| async {
687
688 let _ = std::fs::rename(network::NETWORK_INTERFACES_NEW_FILENAME, network::NETWORK_INTERFACES_FILENAME);
689
690 network::network_reload()?;
691 Ok(())
692 })?;
693
694 Ok(upid_str)
695}
696
697#[api(
698 protected: true,
699 input: {
700 properties: {
701 node: {
702 schema: NODE_SCHEMA,
703 },
704 },
705 },
706 access: {
707 permission: &Permission::Privilege(&["system", "network", "interfaces"], PRIV_SYS_MODIFY, false),
708 },
709)]
710/// Revert network configuration (rm /etc/network/interfaces.new).
711pub fn revert_network_config() -> Result<(), Error> {
712
713 let _ = std::fs::remove_file(network::NETWORK_INTERFACES_NEW_FILENAME);
714
715 Ok(())
716}
717
718const ITEM_ROUTER: Router = Router::new()
719 .get(&API_METHOD_READ_INTERFACE)
720 .put(&API_METHOD_UPDATE_INTERFACE)
721 .delete(&API_METHOD_DELETE_INTERFACE);
722
723pub const ROUTER: Router = Router::new()
724 .get(&API_METHOD_LIST_NETWORK_DEVICES)
725 .put(&API_METHOD_RELOAD_NETWORK_CONFIG)
726 .post(&API_METHOD_CREATE_INTERFACE)
727 .delete(&API_METHOD_REVERT_NETWORK_CONFIG)
728 .match_all("iface", &ITEM_ROUTER);