]> git.proxmox.com Git - pve-manager.git/blob - PVE/API2/Network.pm
20e377813fd25e483b6d10774e0112a1c9cefc39
[pve-manager.git] / PVE / API2 / Network.pm
1 package PVE::API2::Network;
2
3 use strict;
4 use warnings;
5
6 use Net::IP qw(:PROC);
7 use PVE::Tools qw(extract_param dir_glob_regex);
8 use PVE::SafeSyslog;
9 use PVE::INotify;
10 use PVE::Exception qw(raise_param_exc);
11 use PVE::RESTHandler;
12 use PVE::RPCEnvironment;
13 use PVE::JSONSchema qw(get_standard_option);
14 use PVE::AccessControl;
15 use IO::File;
16
17 use base qw(PVE::RESTHandler);
18
19 my $have_sdn;
20 eval {
21 require PVE::Network::SDN::Zones;
22 require PVE::Network::SDN::Controllers;
23 $have_sdn = 1;
24 };
25
26 my $iflockfn = "/etc/network/.pve-interfaces.lock";
27
28 my $bond_mode_enum = [
29 'balance-rr',
30 'active-backup', # OVS and Linux
31 'balance-xor',
32 'broadcast',
33 '802.3ad',
34 'balance-tlb',
35 'balance-alb',
36 'balance-slb', # OVS
37 'lacp-balance-slb', # OVS
38 'lacp-balance-tcp', # OVS
39 ];
40
41 my $network_type_enum = ['bridge', 'bond', 'eth', 'alias', 'vlan',
42 'OVSBridge', 'OVSBond', 'OVSPort', 'OVSIntPort'];
43
44 my $confdesc = {
45 type => {
46 description => "Network interface type",
47 type => 'string',
48 enum => [@$network_type_enum, 'unknown'],
49 },
50 comments => {
51 description => "Comments",
52 type => 'string',
53 optional => 1,
54 },
55 comments6 => {
56 description => "Comments",
57 type => 'string',
58 optional => 1,
59 },
60 autostart => {
61 description => "Automatically start interface on boot.",
62 type => 'boolean',
63 optional => 1,
64 },
65 bridge_vlan_aware => {
66 description => "Enable bridge vlan support.",
67 type => 'boolean',
68 optional => 1,
69 },
70 bridge_ports => {
71 description => "Specify the interfaces you want to add to your bridge.",
72 optional => 1,
73 type => 'string', format => 'pve-iface-list',
74 },
75 ovs_ports => {
76 description => "Specify the interfaces you want to add to your bridge.",
77 optional => 1,
78 type => 'string', format => 'pve-iface-list',
79 },
80 ovs_tag => {
81 description => "Specify a VLan tag (used by OVSPort, OVSIntPort, OVSBond)",
82 optional => 1,
83 type => 'integer',
84 minimum => 1,
85 maximum => 4094,
86 },
87 ovs_options => {
88 description => "OVS interface options.",
89 optional => 1,
90 type => 'string',
91 maxLength => 1024,
92 },
93 ovs_bridge => {
94 description => "The OVS bridge associated with a OVS port. This is required when you create an OVS port.",
95 optional => 1,
96 type => 'string', format => 'pve-iface',
97 },
98 slaves => {
99 description => "Specify the interfaces used by the bonding device.",
100 optional => 1,
101 type => 'string', format => 'pve-iface-list',
102 },
103 ovs_bonds => {
104 description => "Specify the interfaces used by the bonding device.",
105 optional => 1,
106 type => 'string', format => 'pve-iface-list',
107 },
108 bond_mode => {
109 description => "Bonding mode.",
110 optional => 1,
111 type => 'string', enum => $bond_mode_enum,
112 },
113 'bond-primary' => {
114 description => "Specify the primary interface for active-backup bond.",
115 optional => 1,
116 type => 'string', format => 'pve-iface',
117 },
118 bond_xmit_hash_policy => {
119 description => "Selects the transmit hash policy to use for slave selection in balance-xor and 802.3ad modes.",
120 optional => 1,
121 type => 'string',
122 enum => ['layer2', 'layer2+3', 'layer3+4' ],
123 },
124 gateway => {
125 description => 'Default gateway address.',
126 type => 'string', format => 'ipv4',
127 optional => 1,
128 },
129 netmask => {
130 description => 'Network mask.',
131 type => 'string', format => 'ipv4mask',
132 optional => 1,
133 requires => 'address',
134 },
135 address => {
136 description => 'IP address.',
137 type => 'string', format => 'ipv4',
138 optional => 1,
139 requires => 'netmask',
140 },
141 cidr => {
142 description => 'IPv4 CIDR.',
143 type => 'string', format => 'CIDRv4',
144 optional => 1,
145 },
146 mtu => {
147 description => 'MTU.',
148 optional => 1,
149 type => 'integer',
150 minimum => 1280,
151 maximum => 65520,
152 },
153 gateway6 => {
154 description => 'Default ipv6 gateway address.',
155 type => 'string', format => 'ipv6',
156 optional => 1,
157 },
158 netmask6 => {
159 description => 'Network mask.',
160 type => 'integer', minimum => 0, maximum => 128,
161 optional => 1,
162 requires => 'address6',
163 },
164 address6 => {
165 description => 'IP address.',
166 type => 'string', format => 'ipv6',
167 optional => 1,
168 requires => 'netmask6',
169 },
170 cidr6 => {
171 description => 'IPv6 CIDR.',
172 type => 'string', format => 'CIDRv6',
173 optional => 1,
174 },
175 };
176
177 sub json_config_properties {
178 my $prop = shift;
179
180 foreach my $opt (keys %$confdesc) {
181 $prop->{$opt} = $confdesc->{$opt};
182 }
183
184 return $prop;
185 }
186
187 __PACKAGE__->register_method({
188 name => 'index',
189 path => '',
190 method => 'GET',
191 permissions => { user => 'all' },
192 description => "List available networks",
193 proxyto => 'node',
194 parameters => {
195 additionalProperties => 0,
196 properties => {
197 node => get_standard_option('pve-node'),
198 type => {
199 description => "Only list specific interface types.",
200 type => 'string',
201 enum => [ @$network_type_enum, 'any_bridge' ],
202 optional => 1,
203 },
204 },
205 },
206 returns => {
207 type => "array",
208 items => {
209 type => "object",
210 properties => {},
211 },
212 links => [ { rel => 'child', href => "{iface}" } ],
213 },
214 code => sub {
215 my ($param) = @_;
216
217 my $rpcenv = PVE::RPCEnvironment::get();
218
219 my $tmp = PVE::INotify::read_file('interfaces', 1);
220 my $config = $tmp->{data};
221 my $changes = $tmp->{changes};
222
223 $rpcenv->set_result_attrib('changes', $changes) if $changes;
224
225 my $ifaces = $config->{ifaces};
226
227 delete $ifaces->{lo}; # do not list the loopback device
228
229 if ($param->{type}) {
230 foreach my $k (keys %$ifaces) {
231 my $type = $ifaces->{$k}->{type};
232 my $match = ($param->{type} eq $type) || (
233 ($param->{type} eq 'any_bridge') &&
234 ($type eq 'bridge' || $type eq 'OVSBridge'));
235 delete $ifaces->{$k} if !$match;
236 }
237 }
238
239 return PVE::RESTHandler::hash_to_array($ifaces, 'iface');
240 }});
241
242 __PACKAGE__->register_method({
243 name => 'revert_network_changes',
244 path => '',
245 method => 'DELETE',
246 permissions => {
247 check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
248 },
249 protected => 1,
250 description => "Revert network configuration changes.",
251 proxyto => 'node',
252 parameters => {
253 additionalProperties => 0,
254 properties => {
255 node => get_standard_option('pve-node'),
256 },
257 },
258 returns => { type => "null" },
259 code => sub {
260 my ($param) = @_;
261
262 unlink "/etc/network/interfaces.new";
263
264 return undef;
265 }});
266
267 my $check_duplicate = sub {
268 my ($config, $newiface, $key, $name) = @_;
269
270 foreach my $iface (keys %$config) {
271 raise_param_exc({ $key => "$name already exists on interface '$iface'." })
272 if ($newiface ne $iface) && $config->{$iface}->{$key};
273 }
274 };
275
276 my $check_duplicate_gateway = sub {
277 my ($config, $newiface) = @_;
278 return &$check_duplicate($config, $newiface, 'gateway', 'Default gateway');
279 };
280
281 my $check_duplicate_gateway6 = sub {
282 my ($config, $newiface) = @_;
283 return &$check_duplicate($config, $newiface, 'gateway6', 'Default ipv6 gateway');
284 };
285
286 sub ipv6_tobin {
287 return Net::IP::ip_iptobin(Net::IP::ip_expand_address(shift, 6), 6);
288 }
289
290 my $check_ipv6_settings = sub {
291 my ($address, $netmask) = @_;
292
293 raise_param_exc({ netmask => "$netmask is not a valid subnet length for ipv6" })
294 if $netmask < 0 || $netmask > 128;
295
296 raise_param_exc({ address => "$address is not a valid host IPv6 address." })
297 if !Net::IP::ip_is_ipv6($address);
298
299 my $binip = ipv6_tobin($address);
300 my $binmask = Net::IP::ip_get_mask($netmask, 6);
301
302 my $type = ($binip eq $binmask) ? 'ANYCAST' : Net::IP::ip_iptypev6($binip);
303
304 if (defined($type) && $type !~ /^(?:(?:GLOBAL|(?:UNIQUE|LINK)-LOCAL)-UNICAST)$/) {
305 raise_param_exc({ address => "$address with type '$type', cannot be used as host IPv6 address." });
306 }
307 };
308
309 my $map_cidr_to_address_netmask = sub {
310 my ($param) = @_;
311
312 if ($param->{cidr}) {
313 raise_param_exc({ address => "address conflicts with cidr" })
314 if $param->{address};
315 raise_param_exc({ netmask => "netmask conflicts with cidr" })
316 if $param->{netmask};
317
318 my ($address, $netmask) = $param->{cidr} =~ m!^(.*)/(\d+)$!;
319 $param->{address} = $address;
320 $param->{netmask} = $netmask;
321 delete $param->{cidr};
322 }
323
324 if ($param->{cidr6}) {
325 raise_param_exc({ address6 => "address6 conflicts with cidr6" })
326 if $param->{address6};
327 raise_param_exc({ netmask6 => "netmask6 conflicts with cidr6" })
328 if $param->{netmask6};
329
330 my ($address, $netmask) = $param->{cidr6} =~ m!^(.*)/(\d+)$!;
331 $param->{address6} = $address;
332 $param->{netmask6} = $netmask;
333 delete $param->{cidr6};
334 }
335 };
336
337 __PACKAGE__->register_method({
338 name => 'create_network',
339 path => '',
340 method => 'POST',
341 permissions => {
342 check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
343 },
344 description => "Create network device configuration",
345 protected => 1,
346 proxyto => 'node',
347 parameters => {
348 additionalProperties => 0,
349 properties => json_config_properties({
350 node => get_standard_option('pve-node'),
351 iface => get_standard_option('pve-iface')}),
352 },
353 returns => { type => 'null' },
354 code => sub {
355 my ($param) = @_;
356
357 my $node = extract_param($param, 'node');
358 my $iface = extract_param($param, 'iface');
359
360 my $code = sub {
361 my $config = PVE::INotify::read_file('interfaces');
362 my $ifaces = $config->{ifaces};
363
364 raise_param_exc({ iface => "interface already exists" })
365 if $ifaces->{$iface};
366
367 &$check_duplicate_gateway($ifaces, $iface)
368 if $param->{gateway};
369 &$check_duplicate_gateway6($ifaces, $iface)
370 if $param->{gateway6};
371
372 $map_cidr_to_address_netmask->($param);
373
374 &$check_ipv6_settings($param->{address6}, int($param->{netmask6}))
375 if $param->{address6};
376
377 my $families = $param->{families} = [];
378 push @$families, 'inet'
379 if $param->{address} && !grep(/^inet$/, @$families);
380 push @$families, 'inet6'
381 if $param->{address6} && !grep(/^inet6$/, @$families);
382 @$families = ('inet') if !scalar(@$families);
383
384 $param->{method} = $param->{address} ? 'static' : 'manual';
385 $param->{method6} = $param->{address6} ? 'static' : 'manual';
386
387 if ($param->{type} =~ m/^OVS/) {
388 -x '/usr/bin/ovs-vsctl' ||
389 die "Open VSwitch is not installed (need package 'openvswitch-switch')\n";
390 }
391
392 if ($param->{type} eq 'OVSIntPort' || $param->{type} eq 'OVSBond') {
393 my $brname = $param->{ovs_bridge};
394 raise_param_exc({ ovs_bridge => "parameter is required" }) if !$brname;
395 my $br = $ifaces->{$brname};
396 raise_param_exc({ ovs_bridge => "bridge '$brname' does not exist" }) if !$br;
397 raise_param_exc({ ovs_bridge => "interface '$brname' is no OVS bridge" })
398 if $br->{type} ne 'OVSBridge';
399
400 my @ports = split (/\s+/, $br->{ovs_ports} || '');
401 $br->{ovs_ports} = join(' ', @ports, $iface)
402 if ! grep { $_ eq $iface } @ports;
403 }
404
405 $ifaces->{$iface} = $param;
406
407 PVE::INotify::write_file('interfaces', $config);
408 };
409
410 PVE::Tools::lock_file($iflockfn, 10, $code);
411 die $@ if $@;
412
413 return undef;
414 }});
415
416 __PACKAGE__->register_method({
417 name => 'update_network',
418 path => '{iface}',
419 method => 'PUT',
420 permissions => {
421 check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
422 },
423 description => "Update network device configuration",
424 protected => 1,
425 proxyto => 'node',
426 parameters => {
427 additionalProperties => 0,
428 properties => json_config_properties({
429 node => get_standard_option('pve-node'),
430 iface => get_standard_option('pve-iface'),
431 delete => {
432 type => 'string', format => 'pve-configid-list',
433 description => "A list of settings you want to delete.",
434 optional => 1,
435 }}),
436 },
437 returns => { type => 'null' },
438 code => sub {
439 my ($param) = @_;
440
441 my $node = extract_param($param, 'node');
442 my $iface = extract_param($param, 'iface');
443 my $delete = extract_param($param, 'delete');
444
445 my $code = sub {
446 my $config = PVE::INotify::read_file('interfaces');
447 my $ifaces = $config->{ifaces};
448
449 raise_param_exc({ iface => "interface does not exist" })
450 if !$ifaces->{$iface};
451
452 my $families = ($param->{families} ||= []);
453 foreach my $k (PVE::Tools::split_list($delete)) {
454 delete $ifaces->{$iface}->{$k};
455 @$families = grep(!/^inet$/, @$families) if $k eq 'address';
456 @$families = grep(!/^inet6$/, @$families) if $k eq 'address6';
457 if ($k eq 'cidr') {
458 delete $ifaces->{$iface}->{netmask};
459 delete $ifaces->{$iface}->{address};
460 } elsif ($k eq 'cidr6') {
461 delete $ifaces->{$iface}->{netmask6};
462 delete $ifaces->{$iface}->{address6};
463 }
464 }
465
466 $map_cidr_to_address_netmask->($param);
467
468 &$check_duplicate_gateway($ifaces, $iface)
469 if $param->{gateway};
470 &$check_duplicate_gateway6($ifaces, $iface)
471 if $param->{gateway6};
472
473 if ($param->{address}) {
474 push @$families, 'inet' if !grep(/^inet$/, @$families);
475 } else {
476 @$families = grep(!/^inet$/, @$families);
477 }
478 if ($param->{address6}) {
479 &$check_ipv6_settings($param->{address6}, int($param->{netmask6}));
480 push @$families, 'inet6' if !grep(/^inet6$/, @$families);
481 } else {
482 @$families = grep(!/^inet6$/, @$families);
483 }
484 @$families = ('inet') if !scalar(@$families);
485
486 $param->{method} = $param->{address} ? 'static' : 'manual';
487 $param->{method6} = $param->{address6} ? 'static' : 'manual';
488
489 foreach my $k (keys %$param) {
490 $ifaces->{$iface}->{$k} = $param->{$k};
491 }
492
493 PVE::INotify::write_file('interfaces', $config);
494 };
495
496 PVE::Tools::lock_file($iflockfn, 10, $code);
497 die $@ if $@;
498
499 return undef;
500 }});
501
502 __PACKAGE__->register_method({
503 name => 'network_config',
504 path => '{iface}',
505 method => 'GET',
506 permissions => {
507 check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
508 },
509 description => "Read network device configuration",
510 proxyto => 'node',
511 parameters => {
512 additionalProperties => 0,
513 properties => {
514 node => get_standard_option('pve-node'),
515 iface => get_standard_option('pve-iface'),
516 },
517 },
518 returns => {
519 type => "object",
520 properties => {
521 type => {
522 type => 'string',
523 },
524 method => {
525 type => 'string',
526 },
527 },
528 },
529 code => sub {
530 my ($param) = @_;
531
532 my $config = PVE::INotify::read_file('interfaces');
533 my $ifaces = $config->{ifaces};
534
535 raise_param_exc({ iface => "interface does not exist" })
536 if !$ifaces->{$param->{iface}};
537
538 return $ifaces->{$param->{iface}};
539 }});
540
541 __PACKAGE__->register_method({
542 name => 'reload_network_config',
543 path => '',
544 method => 'PUT',
545 permissions => {
546 check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
547 },
548 description => "Reload network configuration",
549 protected => 1,
550 proxyto => 'node',
551 parameters => {
552 additionalProperties => 0,
553 properties => {
554 node => get_standard_option('pve-node'),
555 },
556 },
557 returns => { type => 'string' },
558 code => sub {
559
560 my ($param) = @_;
561
562 my $rpcenv = PVE::RPCEnvironment::get();
563
564 my $authuser = $rpcenv->get_user();
565
566 my $current_config_file = "/etc/network/interfaces";
567 my $new_config_file = "/etc/network/interfaces.new";
568
569 die "you need ifupdown2 to reload networking\n" if !-e '/usr/share/ifupdown2';
570
571 if (-x '/usr/bin/ovs-vsctl') {
572 my $ovs_configured = sub {
573 my $ifaces = shift;
574 my @ovstypes = grep { $_->{type} =~ /^ovs\S+/i } values %$ifaces;
575 return scalar(@ovstypes) > 0;
576 };
577 my $tmp = PVE::INotify::read_file('interfaces', 1);
578 my $ifaces = $tmp->{data}->{ifaces};
579 my $changes = $tmp->{changes};
580
581 if ($ovs_configured->($ifaces)) {
582 die "There are OpenVSwitch configured interfaces, but ifupdown2 ".
583 " reload is not compatible with openvswitch currently\n";
584 } elsif ($changes && $changes =~ /^\s*(?:[+-])?\s*(ovs_type|allow-ovs)/mi) {
585 die "Changes include OpenVSwitch interfaces, but ifupdown2 ".
586 "reload is not compatible with openvswitch currently\n";
587 }
588 }
589
590 my $worker = sub {
591
592 rename($new_config_file, $current_config_file) if -e $new_config_file;
593
594 if ($have_sdn) {
595 my $network_sdn_config = PVE::Network::SDN::Zones::generate_etc_network_config();
596 PVE::Network::SDN::Zones::write_etc_network_config($network_sdn_config);
597 }
598
599 my $err = sub {
600 my $line = shift;
601 if ($line =~ /(warning|error): (\S+):/) {
602 print "$2 : $line \n";
603 }
604 };
605 PVE::Tools::run_command(['ifreload', '-a'], errfunc => $err);
606
607 if ($have_sdn) {
608 my $controller_config = PVE::Network::SDN::Controllers::generate_controller_config();
609 PVE::Network::SDN::Controllers::write_controller_config($controller_config) if ($controller_config);
610 PVE::Network::SDN::Controllers::reload_controller();
611 }
612 };
613 return $rpcenv->fork_worker('srvreload', 'networking', $authuser, $worker);
614 }});
615
616 __PACKAGE__->register_method({
617 name => 'delete_network',
618 path => '{iface}',
619 method => 'DELETE',
620 permissions => {
621 check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
622 },
623 description => "Delete network device configuration",
624 protected => 1,
625 proxyto => 'node',
626 parameters => {
627 additionalProperties => 0,
628 properties => {
629 node => get_standard_option('pve-node'),
630 iface => get_standard_option('pve-iface'),
631 },
632 },
633 returns => { type => 'null' },
634 code => sub {
635 my ($param) = @_;
636
637 my $code = sub {
638 my $config = PVE::INotify::read_file('interfaces');
639 my $ifaces = $config->{ifaces};
640
641 raise_param_exc({ iface => "interface does not exist" })
642 if !$ifaces->{$param->{iface}};
643
644 my $d = $ifaces->{$param->{iface}};
645 if ($d->{type} eq 'OVSIntPort' || $d->{type} eq 'OVSBond') {
646 if (my $brname = $d->{ovs_bridge}) {
647 if (my $br = $ifaces->{$brname}) {
648 if ($br->{ovs_ports}) {
649 my @ports = split (/\s+/, $br->{ovs_ports});
650 my @new = grep { $_ ne $param->{iface} } @ports;
651 $br->{ovs_ports} = join(' ', @new);
652 }
653 }
654 }
655 }
656
657 delete $ifaces->{$param->{iface}};
658
659 PVE::INotify::write_file('interfaces', $config);
660 };
661
662 PVE::Tools::lock_file($iflockfn, 10, $code);
663 die $@ if $@;
664
665 return undef;
666 }});