]> git.proxmox.com Git - pve-manager.git/blob - PVE/API2/Nodes.pm
3bc17534630677120473009811dbb109a064c066
[pve-manager.git] / PVE / API2 / Nodes.pm
1 package PVE::API2::Nodes::Nodeinfo;
2
3 use strict;
4 use warnings;
5
6 use Digest::MD5;
7 use Digest::SHA;
8 use Filesys::Df;
9 use HTTP::Status qw(:constants);
10 use JSON;
11 use POSIX qw(LONG_MAX);
12 use Time::Local qw(timegm_nocheck);
13 use Socket;
14 use IO::Socket::SSL;
15
16 use PVE::API2Tools;
17 use PVE::APLInfo;
18 use PVE::AccessControl;
19 use PVE::Cluster qw(cfs_read_file);
20 use PVE::DataCenterConfig;
21 use PVE::Exception qw(raise raise_perm_exc raise_param_exc);
22 use PVE::Firewall;
23 use PVE::HA::Config;
24 use PVE::HA::Env::PVE2;
25 use PVE::INotify;
26 use PVE::JSONSchema qw(get_standard_option);
27 use PVE::LXC;
28 use PVE::NodeConfig;
29 use PVE::ProcFSTools;
30 use PVE::QemuConfig;
31 use PVE::QemuServer;
32 use PVE::RESTEnvironment qw(log_warn);
33 use PVE::RESTHandler;
34 use PVE::RPCEnvironment;
35 use PVE::RRD;
36 use PVE::Report;
37 use PVE::SafeSyslog;
38 use PVE::Storage;
39 use PVE::Tools qw(file_get_contents);
40 use PVE::pvecfg;
41
42 use PVE::API2::APT;
43 use PVE::API2::Capabilities;
44 use PVE::API2::Ceph;
45 use PVE::API2::Certificates;
46 use PVE::API2::Disks;
47 use PVE::API2::Firewall::Host;
48 use PVE::API2::Hardware;
49 use PVE::API2::LXC::Status;
50 use PVE::API2::LXC;
51 use PVE::API2::Network;
52 use PVE::API2::NodeConfig;
53 use PVE::API2::Qemu::CPU;
54 use PVE::API2::Qemu;
55 use PVE::API2::Replication;
56 use PVE::API2::Services;
57 use PVE::API2::Storage::Scan;
58 use PVE::API2::Storage::Status;
59 use PVE::API2::Subscription;
60 use PVE::API2::Tasks;
61 use PVE::API2::VZDump;
62
63 my $have_sdn;
64 eval {
65 require PVE::API2::Network::SDN::Zones::Status;
66 $have_sdn = 1;
67 };
68
69 use base qw(PVE::RESTHandler);
70
71 my $verify_command_item_desc = {
72 description => "An array of objects describing endpoints, methods and arguments.",
73 type => "array",
74 items => {
75 type => "object",
76 properties => {
77 path => {
78 description => "A relative path to an API endpoint on this node.",
79 type => "string",
80 optional => 0,
81 },
82 method => {
83 description => "A method related to the API endpoint (GET, POST etc.).",
84 type => "string",
85 pattern => "(GET|POST|PUT|DELETE)",
86 optional => 0,
87 },
88 args => {
89 description => "A set of parameter names and their values.",
90 type => "object",
91 optional => 1,
92 },
93 },
94 }
95 };
96
97 PVE::JSONSchema::register_format('pve-command-batch', \&verify_command_batch);
98 sub verify_command_batch {
99 my ($value, $noerr) = @_;
100 my $commands = eval { decode_json($value); };
101
102 return if $noerr && $@;
103 die "commands param did not contain valid JSON: $@" if $@;
104
105 eval { PVE::JSONSchema::validate($commands, $verify_command_item_desc) };
106
107 return $commands if !$@;
108
109 return if $noerr;
110 die "commands is not a valid array of commands: $@";
111 }
112
113 __PACKAGE__->register_method ({
114 subclass => "PVE::API2::Qemu",
115 path => 'qemu',
116 });
117
118 __PACKAGE__->register_method ({
119 subclass => "PVE::API2::LXC",
120 path => 'lxc',
121 });
122
123 __PACKAGE__->register_method ({
124 subclass => "PVE::API2::Ceph",
125 path => 'ceph',
126 });
127
128 __PACKAGE__->register_method ({
129 subclass => "PVE::API2::VZDump",
130 path => 'vzdump',
131 });
132
133 __PACKAGE__->register_method ({
134 subclass => "PVE::API2::Services",
135 path => 'services',
136 });
137
138 __PACKAGE__->register_method ({
139 subclass => "PVE::API2::Subscription",
140 path => 'subscription',
141 });
142
143 __PACKAGE__->register_method ({
144 subclass => "PVE::API2::Network",
145 path => 'network',
146 });
147
148 __PACKAGE__->register_method ({
149 subclass => "PVE::API2::Tasks",
150 path => 'tasks',
151 });
152
153 __PACKAGE__->register_method ({
154 subclass => "PVE::API2::Storage::Scan",
155 path => 'scan',
156 });
157
158 __PACKAGE__->register_method ({
159 subclass => "PVE::API2::Hardware",
160 path => 'hardware',
161 });
162
163 __PACKAGE__->register_method ({
164 subclass => "PVE::API2::Capabilities",
165 path => 'capabilities',
166 });
167
168 __PACKAGE__->register_method ({
169 subclass => "PVE::API2::Storage::Status",
170 path => 'storage',
171 });
172
173 __PACKAGE__->register_method ({
174 subclass => "PVE::API2::Disks",
175 path => 'disks',
176 });
177
178 __PACKAGE__->register_method ({
179 subclass => "PVE::API2::APT",
180 path => 'apt',
181 });
182
183 __PACKAGE__->register_method ({
184 subclass => "PVE::API2::Firewall::Host",
185 path => 'firewall',
186 });
187
188 __PACKAGE__->register_method ({
189 subclass => "PVE::API2::Replication",
190 path => 'replication',
191 });
192
193 __PACKAGE__->register_method ({
194 subclass => "PVE::API2::Certificates",
195 path => 'certificates',
196 });
197
198
199 __PACKAGE__->register_method ({
200 subclass => "PVE::API2::NodeConfig",
201 path => 'config',
202 });
203
204 if ($have_sdn) {
205 __PACKAGE__->register_method ({
206 subclass => "PVE::API2::Network::SDN::Zones::Status",
207 path => 'sdn/zones',
208 });
209
210 __PACKAGE__->register_method ({
211 name => 'sdnindex',
212 path => 'sdn',
213 method => 'GET',
214 permissions => { user => 'all' },
215 description => "SDN index.",
216 parameters => {
217 additionalProperties => 0,
218 properties => {
219 node => get_standard_option('pve-node'),
220 },
221 },
222 returns => {
223 type => 'array',
224 items => {
225 type => "object",
226 properties => {},
227 },
228 links => [ { rel => 'child', href => "{name}" } ],
229 },
230 code => sub {
231 my ($param) = @_;
232
233 my $result = [
234 { name => 'zones' },
235 ];
236 return $result;
237 }});
238 }
239
240 __PACKAGE__->register_method ({
241 name => 'index',
242 path => '',
243 method => 'GET',
244 permissions => { user => 'all' },
245 description => "Node index.",
246 parameters => {
247 additionalProperties => 0,
248 properties => {
249 node => get_standard_option('pve-node'),
250 },
251 },
252 returns => {
253 type => 'array',
254 items => {
255 type => "object",
256 properties => {},
257 },
258 links => [ { rel => 'child', href => "{name}" } ],
259 },
260 code => sub {
261 my ($param) = @_;
262
263 my $result = [
264 { name => 'aplinfo' },
265 { name => 'apt' },
266 { name => 'capabilities' },
267 { name => 'ceph' },
268 { name => 'certificates' },
269 { name => 'config' },
270 { name => 'disks' },
271 { name => 'dns' },
272 { name => 'firewall' },
273 { name => 'hardware' },
274 { name => 'hosts' },
275 { name => 'journal' },
276 { name => 'lxc' },
277 { name => 'migrateall' },
278 { name => 'netstat' },
279 { name => 'network' },
280 { name => 'qemu' },
281 { name => 'query-url-metadata' },
282 { name => 'replication' },
283 { name => 'report' },
284 { name => 'rrd' }, # fixme: remove?
285 { name => 'rrddata' },# fixme: remove?
286 { name => 'scan' },
287 { name => 'services' },
288 { name => 'spiceshell' },
289 { name => 'startall' },
290 { name => 'status' },
291 { name => 'stopall' },
292 { name => 'storage' },
293 { name => 'subscription' },
294 { name => 'suspendall' },
295 { name => 'syslog' },
296 { name => 'tasks' },
297 { name => 'termproxy' },
298 { name => 'time' },
299 { name => 'version' },
300 { name => 'vncshell' },
301 { name => 'vzdump' },
302 { name => 'wakeonlan' },
303 ];
304
305 push @$result, { name => 'sdn' } if $have_sdn;
306
307 return $result;
308 }});
309
310 __PACKAGE__->register_method ({
311 name => 'version',
312 path => 'version',
313 method => 'GET',
314 proxyto => 'node',
315 permissions => { user => 'all' },
316 description => "API version details",
317 parameters => {
318 additionalProperties => 0,
319 properties => {
320 node => get_standard_option('pve-node'),
321 },
322 },
323 returns => {
324 type => "object",
325 properties => {
326 version => {
327 type => 'string',
328 description => 'The current installed pve-manager package version',
329 },
330 release => {
331 type => 'string',
332 description => 'The current installed Proxmox VE Release',
333 },
334 repoid => {
335 type => 'string',
336 description => 'The short git commit hash ID from which this version was build',
337 },
338 },
339 },
340 code => sub {
341 my ($resp, $param) = @_;
342
343 return PVE::pvecfg::version_info();
344 }});
345
346 my sub get_current_kernel_info {
347 my ($sysname, $nodename, $release, $version, $machine) = POSIX::uname();
348
349 my $kernel_version_string = "$sysname $release $version"; # for legacy compat
350 my $current_kernel = {
351 sysname => $sysname,
352 release => $release,
353 version => $version,
354 machine => $machine,
355 };
356 return ($current_kernel, $kernel_version_string);
357 }
358
359 my $boot_mode_info_cache;
360 my sub get_boot_mode_info {
361 return $boot_mode_info_cache if defined($boot_mode_info_cache);
362
363 my $is_efi_booted = -d "/sys/firmware/efi";
364
365 $boot_mode_info_cache = {
366 mode => $is_efi_booted ? 'efi' : 'legacy-bios',
367 };
368
369 my $efi_var = "/sys/firmware/efi/efivars/SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c";
370
371 if ($is_efi_booted && -e $efi_var) {
372 my $efi_var_sec_boot_entry = eval { file_get_contents($efi_var) };
373 if ($@) {
374 warn "Failed to read secure boot state: $@\n";
375 } else {
376 my @secureboot = unpack("CCCCC", $efi_var_sec_boot_entry);
377 $boot_mode_info_cache->{secureboot} = $secureboot[4] == 1 ? 1 : 0;
378 }
379 }
380 return $boot_mode_info_cache;
381 }
382
383 __PACKAGE__->register_method({
384 name => 'status',
385 path => 'status',
386 method => 'GET',
387 permissions => {
388 check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
389 },
390 description => "Read node status",
391 proxyto => 'node',
392 parameters => {
393 additionalProperties => 0,
394 properties => {
395 node => get_standard_option('pve-node'),
396 },
397 },
398 returns => {
399 type => "object",
400 additionalProperties => 1,
401 properties => {
402 # TODO: document remaing ones
403 'boot-info' => {
404 description => "Meta-information about the boot mode.",
405 type => 'object',
406 properties => {
407 mode => {
408 description => 'Through which firmware the system got booted.',
409 type => 'string',
410 enum => [qw(efi legacy-bios)],
411 },
412 secureboot => {
413 description => 'System is booted in secure mode, only applicable for the "efi" mode.',
414 type => 'boolean',
415 optional => 1,
416 },
417 },
418 },
419 'current-kernel' => {
420 description => "The uptime of the system in seconds.",
421 type => 'object',
422 properties => {
423 sysname => {
424 description => 'OS kernel name (e.g., "Linux")',
425 type => 'string',
426 },
427 release => {
428 description => 'OS kernel release (e.g., "6.8.0")',
429 type => 'string',
430 },
431 version => {
432 description => 'OS kernel version with build info',
433 type => 'string',
434 },
435 machine => {
436 description => 'Hardware (architecture) type',
437 type => 'string',
438 },
439 },
440 },
441 },
442 },
443 code => sub {
444 my ($param) = @_;
445
446 my $res = {
447 uptime => 0,
448 idle => 0,
449 };
450
451 my ($uptime, $idle) = PVE::ProcFSTools::read_proc_uptime();
452 $res->{uptime} = $uptime;
453
454 my ($avg1, $avg5, $avg15) = PVE::ProcFSTools::read_loadavg();
455 $res->{loadavg} = [ $avg1, $avg5, $avg15];
456
457 my ($current_kernel_info, $kversion_string) = get_current_kernel_info();
458 $res->{kversion} = $kversion_string;
459 $res->{'current-kernel'} = $current_kernel_info;
460
461 $res->{'boot-info'} = get_boot_mode_info();
462
463 $res->{cpuinfo} = PVE::ProcFSTools::read_cpuinfo();
464
465 my $stat = PVE::ProcFSTools::read_proc_stat();
466 $res->{cpu} = $stat->{cpu};
467 $res->{wait} = $stat->{wait};
468
469 my $meminfo = PVE::ProcFSTools::read_meminfo();
470 $res->{memory} = {
471 free => $meminfo->{memfree},
472 total => $meminfo->{memtotal},
473 used => $meminfo->{memused},
474 };
475
476 $res->{ksm} = {
477 shared => $meminfo->{memshared},
478 };
479
480 $res->{swap} = {
481 free => $meminfo->{swapfree},
482 total => $meminfo->{swaptotal},
483 used => $meminfo->{swapused},
484 };
485
486 $res->{pveversion} = PVE::pvecfg::package() . "/" .
487 PVE::pvecfg::version_text();
488
489 my $dinfo = df('/', 1); # output is bytes
490
491 $res->{rootfs} = {
492 total => $dinfo->{blocks},
493 avail => $dinfo->{bavail},
494 used => $dinfo->{used},
495 free => $dinfo->{blocks} - $dinfo->{used},
496 };
497
498 return $res;
499 }});
500
501 __PACKAGE__->register_method({
502 name => 'netstat',
503 path => 'netstat',
504 method => 'GET',
505 permissions => {
506 check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
507 },
508 description => "Read tap/vm network device interface counters",
509 proxyto => 'node',
510 parameters => {
511 additionalProperties => 0,
512 properties => {
513 node => get_standard_option('pve-node'),
514 },
515 },
516 returns => {
517 type => "array",
518 items => {
519 type => "object",
520 properties => {},
521 },
522 },
523 code => sub {
524 my ($param) = @_;
525
526 my $res = [ ];
527
528 my $netdev = PVE::ProcFSTools::read_proc_net_dev();
529 foreach my $dev (sort keys %$netdev) {
530 next if $dev !~ m/^(?:tap|veth)([1-9]\d*)i(\d+)$/;
531 my ($vmid, $netid) = ($1, $2);
532
533 push @$res, {
534 vmid => $vmid,
535 dev => "net$netid",
536 in => $netdev->{$dev}->{transmit},
537 out => $netdev->{$dev}->{receive},
538 };
539 }
540
541 return $res;
542 }});
543
544 __PACKAGE__->register_method({
545 name => 'execute',
546 path => 'execute',
547 method => 'POST',
548 description => "Execute multiple commands in order, root only.",
549 proxyto => 'node',
550 protected => 1, # avoid problems with proxy code
551 parameters => {
552 additionalProperties => 0,
553 properties => {
554 node => get_standard_option('pve-node'),
555 commands => {
556 description => "JSON encoded array of commands.",
557 type => "string",
558 verbose_description => "JSON encoded array of commands, where each command is an object with the following properties:\n"
559 . PVE::RESTHandler::dump_properties($verify_command_item_desc->{items}->{properties}, 'full'),
560 format => "pve-command-batch",
561 }
562 },
563 },
564 returns => {
565 type => 'array',
566 items => {
567 type => "object",
568 properties => {},
569 },
570 },
571 code => sub {
572 my ($param) = @_;
573 my $res = [];
574
575 my $rpcenv = PVE::RPCEnvironment::get();
576 my $user = $rpcenv->get_user();
577 # just parse the json again, it should already be validated
578 my $commands = eval { decode_json($param->{commands}); };
579
580 foreach my $cmd (@$commands) {
581 eval {
582 $cmd->{args} //= {};
583
584 my $path = "nodes/$param->{node}/$cmd->{path}";
585
586 my $uri_param = {};
587 my ($handler, $info) = PVE::API2->find_handler($cmd->{method}, $path, $uri_param);
588 if (!$handler || !$info) {
589 die "no handler for '$path'\n";
590 }
591
592 foreach my $p (keys %{$cmd->{args}}) {
593 raise_param_exc({ $p => "duplicate parameter" }) if defined($uri_param->{$p});
594 $uri_param->{$p} = $cmd->{args}->{$p};
595 }
596
597 # check access permissions
598 $rpcenv->check_api2_permissions($info->{permissions}, $user, $uri_param);
599
600 push @$res, {
601 status => HTTP_OK,
602 data => $handler->handle($info, $uri_param),
603 };
604 };
605 if (my $err = $@) {
606 my $resp = { status => HTTP_INTERNAL_SERVER_ERROR };
607 if (ref($err) eq "PVE::Exception") {
608 $resp->{status} = $err->{code} if $err->{code};
609 $resp->{errors} = $err->{errors} if $err->{errors};
610 $resp->{message} = $err->{msg};
611 } else {
612 $resp->{message} = $err;
613 }
614 push @$res, $resp;
615 }
616 }
617
618 return $res;
619 }});
620
621
622 __PACKAGE__->register_method({
623 name => 'node_cmd',
624 path => 'status',
625 method => 'POST',
626 permissions => {
627 check => ['perm', '/nodes/{node}', [ 'Sys.PowerMgmt' ]],
628 },
629 protected => 1,
630 description => "Reboot or shutdown a node.",
631 proxyto => 'node',
632 parameters => {
633 additionalProperties => 0,
634 properties => {
635 node => get_standard_option('pve-node'),
636 command => {
637 description => "Specify the command.",
638 type => 'string',
639 enum => [qw(reboot shutdown)],
640 },
641 },
642 },
643 returns => { type => "null" },
644 code => sub {
645 my ($param) = @_;
646
647 if ($param->{command} eq 'reboot') {
648 system ("(sleep 2;/sbin/reboot)&");
649 } elsif ($param->{command} eq 'shutdown') {
650 system ("(sleep 2;/sbin/poweroff)&");
651 }
652
653 return;
654 }});
655
656 __PACKAGE__->register_method({
657 name => 'wakeonlan',
658 path => 'wakeonlan',
659 method => 'POST',
660 permissions => {
661 check => ['perm', '/nodes/{node}', [ 'Sys.PowerMgmt' ]],
662 },
663 protected => 1,
664 description => "Try to wake a node via 'wake on LAN' network packet.",
665 parameters => {
666 additionalProperties => 0,
667 properties => {
668 node => get_standard_option('pve-node', {
669 description => 'target node for wake on LAN packet',
670 completion => sub {
671 my $members = PVE::Cluster::get_members();
672 return [ grep { !$members->{$_}->{online} } keys %$members ];
673 }
674 }),
675 },
676 },
677 returns => {
678 type => 'string',
679 format => 'mac-addr',
680 description => 'MAC address used to assemble the WoL magic packet.',
681 },
682 code => sub {
683 my ($param) = @_;
684
685 my $node = $param->{node};
686 my $local_node = PVE::INotify::nodename();
687
688 die "'$node' is local node, cannot wake my self!\n"
689 if $node eq 'localhost' || $node eq $local_node;
690
691 PVE::Cluster::check_node_exists($node);
692
693 my $config = PVE::NodeConfig::load_config($node);
694 my $wol_config = PVE::NodeConfig::get_wakeonlan_config($config);
695 my $mac_addr = $wol_config->{mac};
696 if (!defined($mac_addr)) {
697 die "No wake on LAN MAC address defined for '$node'!\n";
698 }
699
700 my $local_config = PVE::NodeConfig::load_config($local_node);
701 my $local_wol_config = PVE::NodeConfig::get_wakeonlan_config($local_config);
702 my $broadcast_addr = $local_wol_config->{'broadcast-address'} // '255.255.255.255';
703
704 $mac_addr =~ s/://g;
705 my $packet = chr(0xff) x 6 . pack('H*', $mac_addr) x 16;
706
707 my $addr = gethostbyname($broadcast_addr);
708 my $port = getservbyname('discard', 'udp');
709 my $to = Socket::pack_sockaddr_in($port, $addr);
710
711 socket(my $sock, Socket::AF_INET, Socket::SOCK_DGRAM, Socket::IPPROTO_UDP)
712 || die "Unable to open socket: $!\n";
713 setsockopt($sock, Socket::SOL_SOCKET, Socket::SO_BROADCAST, 1)
714 || die "Unable to set socket option: $!\n";
715
716 if (defined(my $bind_iface = $local_wol_config->{'bind-interface'})) {
717 my $bind_iface_raw = pack('Z*', $bind_iface); # Null terminated interface name
718 setsockopt($sock, Socket::SOL_SOCKET, Socket::SO_BINDTODEVICE, $bind_iface_raw)
719 || die "Unable to bind socket to interface '$bind_iface': $!\n";
720 }
721
722 send($sock, $packet, 0, $to)
723 || die "Unable to send packet: $!\n";
724
725 close($sock);
726
727 return $wol_config->{mac};
728 }});
729
730 __PACKAGE__->register_method({
731 name => 'rrd',
732 path => 'rrd',
733 method => 'GET',
734 protected => 1, # fixme: can we avoid that?
735 permissions => {
736 check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
737 },
738 description => "Read node RRD statistics (returns PNG)",
739 parameters => {
740 additionalProperties => 0,
741 properties => {
742 node => get_standard_option('pve-node'),
743 timeframe => {
744 description => "Specify the time frame you are interested in.",
745 type => 'string',
746 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
747 },
748 ds => {
749 description => "The list of datasources you want to display.",
750 type => 'string', format => 'pve-configid-list',
751 },
752 cf => {
753 description => "The RRD consolidation function",
754 type => 'string',
755 enum => [ 'AVERAGE', 'MAX' ],
756 optional => 1,
757 },
758 },
759 },
760 returns => {
761 type => "object",
762 properties => {
763 filename => { type => 'string' },
764 },
765 },
766 code => sub {
767 my ($param) = @_;
768
769 return PVE::RRD::create_rrd_graph(
770 "pve2-node/$param->{node}", $param->{timeframe},
771 $param->{ds}, $param->{cf});
772
773 }});
774
775 __PACKAGE__->register_method({
776 name => 'rrddata',
777 path => 'rrddata',
778 method => 'GET',
779 protected => 1, # fixme: can we avoid that?
780 permissions => {
781 check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
782 },
783 description => "Read node RRD statistics",
784 parameters => {
785 additionalProperties => 0,
786 properties => {
787 node => get_standard_option('pve-node'),
788 timeframe => {
789 description => "Specify the time frame you are interested in.",
790 type => 'string',
791 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
792 },
793 cf => {
794 description => "The RRD consolidation function",
795 type => 'string',
796 enum => [ 'AVERAGE', 'MAX' ],
797 optional => 1,
798 },
799 },
800 },
801 returns => {
802 type => "array",
803 items => {
804 type => "object",
805 properties => {},
806 },
807 },
808 code => sub {
809 my ($param) = @_;
810
811 return PVE::RRD::create_rrd_data(
812 "pve2-node/$param->{node}", $param->{timeframe}, $param->{cf});
813 }});
814
815 __PACKAGE__->register_method({
816 name => 'syslog',
817 path => 'syslog',
818 method => 'GET',
819 description => "Read system log",
820 proxyto => 'node',
821 permissions => {
822 check => ['perm', '/nodes/{node}', [ 'Sys.Syslog' ]],
823 },
824 protected => 1,
825 parameters => {
826 additionalProperties => 0,
827 properties => {
828 node => get_standard_option('pve-node'),
829 start => {
830 type => 'integer',
831 minimum => 0,
832 optional => 1,
833 },
834 limit => {
835 type => 'integer',
836 minimum => 0,
837 optional => 1,
838 },
839 since => {
840 type=> 'string',
841 pattern => '^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}(:\d{2})?)?$',
842 description => "Display all log since this date-time string.",
843 optional => 1,
844 },
845 until => {
846 type=> 'string',
847 pattern => '^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}(:\d{2})?)?$',
848 description => "Display all log until this date-time string.",
849 optional => 1,
850 },
851 service => {
852 description => "Service ID",
853 type => 'string',
854 maxLength => 128,
855 optional => 1,
856 },
857 },
858 },
859 returns => {
860 type => 'array',
861 items => {
862 type => "object",
863 properties => {
864 n => {
865 description=> "Line number",
866 type=> 'integer',
867 },
868 t => {
869 description=> "Line text",
870 type => 'string',
871 }
872 }
873 }
874 },
875 code => sub {
876 my ($param) = @_;
877
878 my $rpcenv = PVE::RPCEnvironment::get();
879 my $user = $rpcenv->get_user();
880 my $node = $param->{node};
881 my $service;
882
883 if ($param->{service}) {
884 my $service_aliases = {
885 'postfix' => 'postfix@-',
886 };
887
888 $service = $service_aliases->{$param->{service}} // $param->{service};
889 }
890
891 my ($count, $lines) = PVE::Tools::dump_journal($param->{start}, $param->{limit},
892 $param->{since}, $param->{until}, $service);
893
894 $rpcenv->set_result_attrib('total', $count);
895
896 return $lines;
897 }});
898
899 __PACKAGE__->register_method({
900 name => 'journal',
901 path => 'journal',
902 method => 'GET',
903 description => "Read Journal",
904 proxyto => 'node',
905 permissions => {
906 check => ['perm', '/nodes/{node}', [ 'Sys.Syslog' ]],
907 },
908 protected => 1,
909 parameters => {
910 additionalProperties => 0,
911 properties => {
912 node => get_standard_option('pve-node'),
913 since => {
914 type=> 'integer',
915 minimum => 0,
916 description => "Display all log since this UNIX epoch. Conflicts with 'startcursor'.",
917 optional => 1,
918 },
919 until => {
920 type=> 'integer',
921 minimum => 0,
922 description => "Display all log until this UNIX epoch. Conflicts with 'endcursor'.",
923 optional => 1,
924 },
925 lastentries => {
926 description => "Limit to the last X lines. Conflicts with a range.",
927 type => 'integer',
928 minimum => 0,
929 optional => 1,
930 },
931 startcursor => {
932 description => "Start after the given Cursor. Conflicts with 'since'",
933 type => 'string',
934 optional => 1,
935 },
936 endcursor => {
937 description => "End before the given Cursor. Conflicts with 'until'",
938 type => 'string',
939 optional => 1,
940 },
941 },
942 },
943 returns => {
944 type => 'array',
945 items => {
946 type => "string",
947 }
948 },
949 code => sub {
950 my ($param) = @_;
951
952 my $rpcenv = PVE::RPCEnvironment::get();
953 my $user = $rpcenv->get_user();
954
955 my $cmd = ["/usr/bin/mini-journalreader", "-j"];
956 push @$cmd, '-n', $param->{lastentries} if $param->{lastentries};
957 push @$cmd, '-b', $param->{since} if $param->{since};
958 push @$cmd, '-e', $param->{until} if $param->{until};
959 push @$cmd, '-f', PVE::Tools::shellquote($param->{startcursor}) if $param->{startcursor};
960 push @$cmd, '-t', PVE::Tools::shellquote($param->{endcursor}) if $param->{endcursor};
961 push @$cmd, ' | gzip ';
962
963 open(my $fh, "-|", join(' ', @$cmd))
964 or die "could not start mini-journalreader";
965
966 return {
967 download => {
968 fh => $fh,
969 stream => 1,
970 'content-type' => 'application/json',
971 'content-encoding' => 'gzip',
972 },
973 },
974 }});
975
976 my $sslcert;
977
978 my $shell_cmd_map = {
979 'login' => {
980 cmd => [ '/bin/login', '-f', 'root' ],
981 },
982 'upgrade' => {
983 cmd => [ '/usr/bin/pveupgrade', '--shell' ],
984 },
985 'ceph_install' => {
986 cmd => [ '/usr/bin/pveceph', 'install' ],
987 allow_args => 1,
988 },
989 };
990
991 sub get_shell_command {
992 my ($user, $shellcmd, $args) = @_;
993
994 my $cmd;
995 if ($user eq 'root@pam') {
996 if (defined($shellcmd) && exists($shell_cmd_map->{$shellcmd})) {
997 my $def = $shell_cmd_map->{$shellcmd};
998 $cmd = [ @{$def->{cmd}} ]; # clone
999 if (defined($args) && $def->{allow_args}) {
1000 push @$cmd, split("\0", $args);
1001 }
1002 } else {
1003 $cmd = [ '/bin/login', '-f', 'root' ];
1004 }
1005 } else {
1006 # non-root must always login for now, we do not have a superuser role!
1007 $cmd = [ '/bin/login' ];
1008 }
1009 return $cmd;
1010 }
1011
1012 my $get_vnc_connection_info = sub {
1013 my $node = shift;
1014
1015 my $remote_cmd = [];
1016
1017 my ($remip, $family);
1018 if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
1019 ($remip, $family) = PVE::Cluster::remote_node_ip($node);
1020 $remote_cmd = ['/usr/bin/ssh', '-e', 'none', '-t', $remip , '--'];
1021 } else {
1022 $family = PVE::Tools::get_host_address_family($node);
1023 }
1024 my $port = PVE::Tools::next_vnc_port($family);
1025
1026 return ($port, $remote_cmd);
1027 };
1028
1029 __PACKAGE__->register_method ({
1030 name => 'vncshell',
1031 path => 'vncshell',
1032 method => 'POST',
1033 protected => 1,
1034 permissions => {
1035 check => ['perm', '/nodes/{node}', [ 'Sys.Console' ]],
1036 },
1037 description => "Creates a VNC Shell proxy.",
1038 parameters => {
1039 additionalProperties => 0,
1040 properties => {
1041 node => get_standard_option('pve-node'),
1042 cmd => {
1043 type => 'string',
1044 description => "Run specific command or default to login (requires 'root\@pam')",
1045 enum => [keys %$shell_cmd_map],
1046 optional => 1,
1047 default => 'login',
1048 },
1049 'cmd-opts' => {
1050 type => 'string',
1051 description => "Add parameters to a command. Encoded as null terminated strings.",
1052 requires => 'cmd',
1053 optional => 1,
1054 default => '',
1055 },
1056 websocket => {
1057 optional => 1,
1058 type => 'boolean',
1059 description => "use websocket instead of standard vnc.",
1060 },
1061 width => {
1062 optional => 1,
1063 description => "sets the width of the console in pixels.",
1064 type => 'integer',
1065 minimum => 16,
1066 maximum => 4096,
1067 },
1068 height => {
1069 optional => 1,
1070 description => "sets the height of the console in pixels.",
1071 type => 'integer',
1072 minimum => 16,
1073 maximum => 2160,
1074 },
1075 },
1076 },
1077 returns => {
1078 additionalProperties => 0,
1079 properties => {
1080 user => { type => 'string' },
1081 ticket => { type => 'string' },
1082 cert => { type => 'string' },
1083 port => { type => 'integer' },
1084 upid => { type => 'string' },
1085 },
1086 },
1087 code => sub {
1088 my ($param) = @_;
1089
1090 my $rpcenv = PVE::RPCEnvironment::get();
1091 my ($user, undef, $realm) = PVE::AccessControl::verify_username($rpcenv->get_user());
1092
1093
1094 if (defined($param->{cmd}) && $param->{cmd} ne 'login' && $user ne 'root@pam') {
1095 raise_perm_exc('user != root@pam');
1096 }
1097
1098 my $node = $param->{node};
1099
1100 my $authpath = "/nodes/$node";
1101 my $ticket = PVE::AccessControl::assemble_vnc_ticket($user, $authpath);
1102
1103 $sslcert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192)
1104 if !$sslcert;
1105
1106 my ($port, $remcmd) = $get_vnc_connection_info->($node);
1107
1108 my $shcmd = get_shell_command($user, $param->{cmd}, $param->{'cmd-opts'});
1109
1110 my $timeout = 10;
1111
1112 my $cmd = ['/usr/bin/vncterm',
1113 '-rfbport', $port,
1114 '-timeout', $timeout,
1115 '-authpath', $authpath,
1116 '-perm', 'Sys.Console',
1117 ];
1118
1119 push @$cmd, '-width', $param->{width} if $param->{width};
1120 push @$cmd, '-height', $param->{height} if $param->{height};
1121
1122 if ($param->{websocket}) {
1123 $ENV{PVE_VNC_TICKET} = $ticket; # pass ticket to vncterm
1124 push @$cmd, '-notls', '-listen', 'localhost';
1125 }
1126
1127 push @$cmd, '-c', @$remcmd, @$shcmd;
1128
1129 my $realcmd = sub {
1130 my $upid = shift;
1131
1132 syslog ('info', "starting vnc proxy $upid\n");
1133
1134 my $cmdstr = join (' ', @$cmd);
1135 syslog ('info', "launch command: $cmdstr");
1136
1137 eval {
1138 foreach my $k (keys %ENV) {
1139 next if $k eq 'PVE_VNC_TICKET';
1140 next if $k eq 'PATH' || $k eq 'TERM' || $k eq 'USER' || $k eq 'HOME' || $k eq 'LANG' || $k eq 'LANGUAGE';
1141 delete $ENV{$k};
1142 }
1143 $ENV{PWD} = '/';
1144
1145 PVE::Tools::run_command($cmd, errmsg => "vncterm failed", keeplocale => 1);
1146 };
1147 if (my $err = $@) {
1148 syslog ('err', $err);
1149 }
1150
1151 return;
1152 };
1153
1154 my $upid = $rpcenv->fork_worker('vncshell', "", $user, $realcmd);
1155
1156 PVE::Tools::wait_for_vnc_port($port);
1157
1158 return {
1159 user => $user,
1160 ticket => $ticket,
1161 port => $port,
1162 upid => $upid,
1163 cert => $sslcert,
1164 };
1165 }});
1166
1167 __PACKAGE__->register_method ({
1168 name => 'termproxy',
1169 path => 'termproxy',
1170 method => 'POST',
1171 protected => 1,
1172 permissions => {
1173 check => ['perm', '/nodes/{node}', [ 'Sys.Console' ]],
1174 },
1175 description => "Creates a VNC Shell proxy.",
1176 parameters => {
1177 additionalProperties => 0,
1178 properties => {
1179 node => get_standard_option('pve-node'),
1180 cmd => {
1181 type => 'string',
1182 description => "Run specific command or default to login (requires 'root\@pam')",
1183 enum => [keys %$shell_cmd_map],
1184 optional => 1,
1185 default => 'login',
1186 },
1187 'cmd-opts' => {
1188 type => 'string',
1189 description => "Add parameters to a command. Encoded as null terminated strings.",
1190 requires => 'cmd',
1191 optional => 1,
1192 default => '',
1193 },
1194 },
1195 },
1196 returns => {
1197 additionalProperties => 0,
1198 properties => {
1199 user => { type => 'string' },
1200 ticket => { type => 'string' },
1201 port => { type => 'integer' },
1202 upid => { type => 'string' },
1203 },
1204 },
1205 code => sub {
1206 my ($param) = @_;
1207
1208 my $rpcenv = PVE::RPCEnvironment::get();
1209 my ($user, undef, $realm) = PVE::AccessControl::verify_username($rpcenv->get_user());
1210
1211 my $node = $param->{node};
1212 my $authpath = "/nodes/$node";
1213 my $ticket = PVE::AccessControl::assemble_vnc_ticket($user, $authpath);
1214
1215 my ($port, $remcmd) = $get_vnc_connection_info->($node);
1216
1217 my $shcmd = get_shell_command($user, $param->{cmd}, $param->{'cmd-opts'});
1218
1219 my $realcmd = sub {
1220 my $upid = shift;
1221
1222 syslog ('info', "starting termproxy $upid\n");
1223
1224 my $cmd = [
1225 '/usr/bin/termproxy',
1226 $port,
1227 '--path', $authpath,
1228 '--perm', 'Sys.Console',
1229 '--'
1230 ];
1231 push @$cmd, @$remcmd, @$shcmd;
1232
1233 PVE::Tools::run_command($cmd);
1234 };
1235 my $upid = $rpcenv->fork_worker('vncshell', "", $user, $realcmd);
1236
1237 PVE::Tools::wait_for_vnc_port($port);
1238
1239 return {
1240 user => $user,
1241 ticket => $ticket,
1242 port => $port,
1243 upid => $upid,
1244 };
1245 }});
1246
1247 __PACKAGE__->register_method({
1248 name => 'vncwebsocket',
1249 path => 'vncwebsocket',
1250 method => 'GET',
1251 permissions => {
1252 description => "You also need to pass a valid ticket (vncticket).",
1253 check => ['perm', '/nodes/{node}', [ 'Sys.Console' ]],
1254 },
1255 description => "Opens a websocket for VNC traffic.",
1256 parameters => {
1257 additionalProperties => 0,
1258 properties => {
1259 node => get_standard_option('pve-node'),
1260 vncticket => {
1261 description => "Ticket from previous call to vncproxy.",
1262 type => 'string',
1263 maxLength => 512,
1264 },
1265 port => {
1266 description => "Port number returned by previous vncproxy call.",
1267 type => 'integer',
1268 minimum => 5900,
1269 maximum => 5999,
1270 },
1271 },
1272 },
1273 returns => {
1274 type => "object",
1275 properties => {
1276 port => { type => 'string' },
1277 },
1278 },
1279 code => sub {
1280 my ($param) = @_;
1281
1282 my $rpcenv = PVE::RPCEnvironment::get();
1283
1284 my ($user, undef, $realm) = PVE::AccessControl::verify_username($rpcenv->get_user());
1285
1286 my $authpath = "/nodes/$param->{node}";
1287
1288 PVE::AccessControl::verify_vnc_ticket($param->{vncticket}, $user, $authpath);
1289
1290 my $port = $param->{port};
1291
1292 return { port => $port };
1293 }});
1294
1295 __PACKAGE__->register_method ({
1296 name => 'spiceshell',
1297 path => 'spiceshell',
1298 method => 'POST',
1299 protected => 1,
1300 proxyto => 'node',
1301 permissions => {
1302 check => ['perm', '/nodes/{node}', [ 'Sys.Console' ]],
1303 },
1304 description => "Creates a SPICE shell.",
1305 parameters => {
1306 additionalProperties => 0,
1307 properties => {
1308 node => get_standard_option('pve-node'),
1309 proxy => get_standard_option('spice-proxy', { optional => 1 }),
1310 cmd => {
1311 type => 'string',
1312 description => "Run specific command or default to login (requires 'root\@pam')",
1313 enum => [keys %$shell_cmd_map],
1314 optional => 1,
1315 default => 'login',
1316 },
1317 'cmd-opts' => {
1318 type => 'string',
1319 description => "Add parameters to a command. Encoded as null terminated strings.",
1320 requires => 'cmd',
1321 optional => 1,
1322 default => '',
1323 },
1324 },
1325 },
1326 returns => get_standard_option('remote-viewer-config'),
1327 code => sub {
1328 my ($param) = @_;
1329
1330 my $rpcenv = PVE::RPCEnvironment::get();
1331 my $authuser = $rpcenv->get_user();
1332
1333 my ($user, undef, $realm) = PVE::AccessControl::verify_username($authuser);
1334
1335
1336 if (defined($param->{cmd}) && $param->{cmd} ne 'login' && $user ne 'root@pam') {
1337 raise_perm_exc('user != root@pam');
1338 }
1339
1340 my $node = $param->{node};
1341 my $proxy = $param->{proxy};
1342
1343 my $authpath = "/nodes/$node";
1344 my $permissions = 'Sys.Console';
1345
1346 my $shcmd = get_shell_command($user, $param->{cmd}, $param->{'cmd-opts'});
1347
1348 my $title = "Shell on '$node'";
1349
1350 return PVE::API2Tools::run_spiceterm($authpath, $permissions, 0, $node, $proxy, $title, $shcmd);
1351 }});
1352
1353 __PACKAGE__->register_method({
1354 name => 'dns',
1355 path => 'dns',
1356 method => 'GET',
1357 permissions => {
1358 check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
1359 },
1360 description => "Read DNS settings.",
1361 proxyto => 'node',
1362 parameters => {
1363 additionalProperties => 0,
1364 properties => {
1365 node => get_standard_option('pve-node'),
1366 },
1367 },
1368 returns => {
1369 type => "object",
1370 additionalProperties => 0,
1371 properties => {
1372 search => {
1373 description => "Search domain for host-name lookup.",
1374 type => 'string',
1375 optional => 1,
1376 },
1377 dns1 => {
1378 description => 'First name server IP address.',
1379 type => 'string',
1380 optional => 1,
1381 },
1382 dns2 => {
1383 description => 'Second name server IP address.',
1384 type => 'string',
1385 optional => 1,
1386 },
1387 dns3 => {
1388 description => 'Third name server IP address.',
1389 type => 'string',
1390 optional => 1,
1391 },
1392 },
1393 },
1394 code => sub {
1395 my ($param) = @_;
1396
1397 my $res = PVE::INotify::read_file('resolvconf');
1398
1399 return $res;
1400 }});
1401
1402 __PACKAGE__->register_method({
1403 name => 'update_dns',
1404 path => 'dns',
1405 method => 'PUT',
1406 description => "Write DNS settings.",
1407 permissions => {
1408 check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
1409 },
1410 proxyto => 'node',
1411 protected => 1,
1412 parameters => {
1413 additionalProperties => 0,
1414 properties => {
1415 node => get_standard_option('pve-node'),
1416 search => {
1417 description => "Search domain for host-name lookup.",
1418 type => 'string',
1419 },
1420 dns1 => {
1421 description => 'First name server IP address.',
1422 type => 'string', format => 'ip',
1423 optional => 1,
1424 },
1425 dns2 => {
1426 description => 'Second name server IP address.',
1427 type => 'string', format => 'ip',
1428 optional => 1,
1429 },
1430 dns3 => {
1431 description => 'Third name server IP address.',
1432 type => 'string', format => 'ip',
1433 optional => 1,
1434 },
1435 },
1436 },
1437 returns => { type => "null" },
1438 code => sub {
1439 my ($param) = @_;
1440
1441 PVE::INotify::update_file('resolvconf', $param);
1442
1443 return;
1444 }});
1445
1446 __PACKAGE__->register_method({
1447 name => 'time',
1448 path => 'time',
1449 method => 'GET',
1450 permissions => {
1451 check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
1452 },
1453 description => "Read server time and time zone settings.",
1454 proxyto => 'node',
1455 parameters => {
1456 additionalProperties => 0,
1457 properties => {
1458 node => get_standard_option('pve-node'),
1459 },
1460 },
1461 returns => {
1462 type => "object",
1463 additionalProperties => 0,
1464 properties => {
1465 timezone => {
1466 description => "Time zone",
1467 type => 'string',
1468 },
1469 time => {
1470 description => "Seconds since 1970-01-01 00:00:00 UTC.",
1471 type => 'integer',
1472 minimum => 1297163644,
1473 renderer => 'timestamp',
1474 },
1475 localtime => {
1476 description => "Seconds since 1970-01-01 00:00:00 (local time)",
1477 type => 'integer',
1478 minimum => 1297163644,
1479 renderer => 'timestamp_gmt',
1480 },
1481 },
1482 },
1483 code => sub {
1484 my ($param) = @_;
1485
1486 my $ctime = time();
1487 my $ltime = timegm_nocheck(localtime($ctime));
1488 my $res = {
1489 timezone => PVE::INotify::read_file('timezone'),
1490 time => $ctime,
1491 localtime => $ltime,
1492 };
1493
1494 return $res;
1495 }});
1496
1497 __PACKAGE__->register_method({
1498 name => 'set_timezone',
1499 path => 'time',
1500 method => 'PUT',
1501 description => "Set time zone.",
1502 permissions => {
1503 check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
1504 },
1505 proxyto => 'node',
1506 protected => 1,
1507 parameters => {
1508 additionalProperties => 0,
1509 properties => {
1510 node => get_standard_option('pve-node'),
1511 timezone => {
1512 description => "Time zone. The file '/usr/share/zoneinfo/zone.tab' contains the list of valid names.",
1513 type => 'string',
1514 },
1515 },
1516 },
1517 returns => { type => "null" },
1518 code => sub {
1519 my ($param) = @_;
1520
1521 PVE::INotify::write_file('timezone', $param->{timezone});
1522
1523 return;
1524 }});
1525
1526 __PACKAGE__->register_method({
1527 name => 'aplinfo',
1528 path => 'aplinfo',
1529 method => 'GET',
1530 permissions => {
1531 user => 'all',
1532 },
1533 description => "Get list of appliances.",
1534 proxyto => 'node',
1535 parameters => {
1536 additionalProperties => 0,
1537 properties => {
1538 node => get_standard_option('pve-node'),
1539 },
1540 },
1541 returns => {
1542 type => 'array',
1543 items => {
1544 type => "object",
1545 properties => {},
1546 },
1547 },
1548 code => sub {
1549 my ($param) = @_;
1550
1551 my $list = PVE::APLInfo::load_data();
1552
1553 my $res = [];
1554 for my $appliance (values %{$list->{all}}) {
1555 next if $appliance->{'package'} eq 'pve-web-news';
1556 push @$res, $appliance;
1557 }
1558
1559 return $res;
1560 }});
1561
1562 __PACKAGE__->register_method({
1563 name => 'apl_download',
1564 path => 'aplinfo',
1565 method => 'POST',
1566 permissions => {
1567 check => ['perm', '/storage/{storage}', ['Datastore.AllocateTemplate']],
1568 },
1569 description => "Download appliance templates.",
1570 proxyto => 'node',
1571 protected => 1,
1572 parameters => {
1573 additionalProperties => 0,
1574 properties => {
1575 node => get_standard_option('pve-node'),
1576 storage => get_standard_option('pve-storage-id', {
1577 description => "The storage where the template will be stored",
1578 completion => \&PVE::Storage::complete_storage_enabled,
1579 }),
1580 template => {
1581 type => 'string',
1582 description => "The template which will downloaded",
1583 maxLength => 255,
1584 completion => \&complete_templet_repo,
1585 },
1586 },
1587 },
1588 returns => { type => "string" },
1589 code => sub {
1590 my ($param) = @_;
1591
1592 my $rpcenv = PVE::RPCEnvironment::get();
1593 my $user = $rpcenv->get_user();
1594
1595 my $node = $param->{node};
1596 my $template = $param->{template};
1597
1598 my $list = PVE::APLInfo::load_data();
1599 my $appliance = $list->{all}->{$template};
1600 raise_param_exc({ template => "no such template"}) if !$appliance;
1601
1602 my $cfg = PVE::Storage::config();
1603 my $scfg = PVE::Storage::storage_check_enabled($cfg, $param->{storage}, $node);
1604
1605 die "unknown template type '$appliance->{type}'\n"
1606 if !($appliance->{type} eq 'openvz' || $appliance->{type} eq 'lxc');
1607
1608 die "storage '$param->{storage}' does not support templates\n"
1609 if !$scfg->{content}->{vztmpl};
1610
1611 my $tmpldir = PVE::Storage::get_vztmpl_dir($cfg, $param->{storage});
1612
1613 my $worker = sub {
1614 my $dccfg = PVE::Cluster::cfs_read_file('datacenter.cfg');
1615
1616 PVE::Tools::download_file_from_url("$tmpldir/$template", $appliance->{location}, {
1617 hash_required => 1,
1618 sha512sum => $appliance->{sha512sum},
1619 md5sum => $appliance->{md5sum},
1620 http_proxy => $dccfg->{http_proxy},
1621 });
1622 };
1623
1624 my $upid = $rpcenv->fork_worker('download', $template, $user, $worker);
1625
1626 return $upid;
1627 }});
1628
1629 __PACKAGE__->register_method({
1630 name => 'query_url_metadata',
1631 path => 'query-url-metadata',
1632 method => 'GET',
1633 description => "Query metadata of an URL: file size, file name and mime type.",
1634 proxyto => 'node',
1635 permissions => {
1636 check => ['or',
1637 ['perm', '/', [ 'Sys.Audit', 'Sys.Modify' ]],
1638 ['perm', '/nodes/{node}', [ 'Sys.AccessNetwork' ]],
1639 ],
1640 },
1641 parameters => {
1642 additionalProperties => 0,
1643 properties => {
1644 node => get_standard_option('pve-node'),
1645 url => {
1646 description => "The URL to query the metadata from.",
1647 type => 'string',
1648 pattern => 'https?://.*',
1649 },
1650 'verify-certificates' => {
1651 description => "If false, no SSL/TLS certificates will be verified.",
1652 type => 'boolean',
1653 optional => 1,
1654 default => 1,
1655 },
1656 },
1657 },
1658 returns => {
1659 type => "object",
1660 properties => {
1661 filename => {
1662 type => 'string',
1663 optional => 1,
1664 },
1665 size => {
1666 type => 'integer',
1667 renderer => 'bytes',
1668 optional => 1,
1669 },
1670 mimetype => {
1671 type => 'string',
1672 optional => 1,
1673 },
1674 },
1675 },
1676 code => sub {
1677 my ($param) = @_;
1678
1679 my $url = $param->{url};
1680
1681 my $ua = LWP::UserAgent->new();
1682 $ua->agent("Proxmox VE");
1683
1684 my $dccfg = PVE::Cluster::cfs_read_file('datacenter.cfg');
1685 if ($dccfg->{http_proxy}) {
1686 $ua->proxy('http', $dccfg->{http_proxy});
1687 }
1688
1689 my $verify = $param->{'verify-certificates'} // 1;
1690 if (!$verify) {
1691 $ua->ssl_opts(
1692 verify_hostname => 0,
1693 SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE,
1694 );
1695 }
1696
1697 my $req = HTTP::Request->new(HEAD => $url);
1698 my $res = $ua->request($req);
1699
1700 die "invalid server response: '" . $res->status_line() . "'\n" if ($res->code() != 200);
1701
1702 my $size = $res->header("Content-Length");
1703 my $disposition = $res->header("Content-Disposition");
1704 my $type = $res->header("Content-Type");
1705
1706 my $filename;
1707
1708 if ($disposition && ($disposition =~ m/filename="([^"]*)"/ || $disposition =~ m/filename=([^;]*)/)) {
1709 $filename = $1;
1710 } elsif ($url =~ m!^[^?]+/([^?/]*)(?:\?.*)?$!) {
1711 $filename = $1;
1712 }
1713
1714 # Content-Type: text/html; charset=utf-8
1715 if ($type && $type =~ m/^([^;]+);/) {
1716 $type = $1;
1717 }
1718
1719 my $ret = {};
1720 $ret->{filename} = $filename if $filename;
1721 $ret->{size} = $size + 0 if $size;
1722 $ret->{mimetype} = $type if $type;
1723
1724 return $ret;
1725 }});
1726
1727 __PACKAGE__->register_method({
1728 name => 'report',
1729 path => 'report',
1730 method => 'GET',
1731 permissions => {
1732 check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
1733 },
1734 protected => 1,
1735 description => "Gather various systems information about a node",
1736 proxyto => 'node',
1737 parameters => {
1738 additionalProperties => 0,
1739 properties => {
1740 node => get_standard_option('pve-node'),
1741 },
1742 },
1743 returns => {
1744 type => 'string',
1745 },
1746 code => sub {
1747 return PVE::Report::generate();
1748 }});
1749
1750 # returns a list of VMIDs, those can be filtered by
1751 # * current parent node
1752 # * vmid whitelist
1753 # * guest is a template (default: skip)
1754 # * guest is HA manged (default: skip)
1755 my $get_filtered_vmlist = sub {
1756 my ($nodename, $vmfilter, $templates, $ha_managed) = @_;
1757
1758 my $vmlist = PVE::Cluster::get_vmlist();
1759
1760 my $vms_allowed;
1761 if (defined($vmfilter)) {
1762 $vms_allowed = { map { $_ => 1 } PVE::Tools::split_list($vmfilter) };
1763 }
1764
1765 my $res = {};
1766 foreach my $vmid (keys %{$vmlist->{ids}}) {
1767 next if defined($vms_allowed) && !$vms_allowed->{$vmid};
1768
1769 my $d = $vmlist->{ids}->{$vmid};
1770 next if $nodename && $d->{node} ne $nodename;
1771
1772 eval {
1773 my $class;
1774 if ($d->{type} eq 'lxc') {
1775 $class = 'PVE::LXC::Config';
1776 } elsif ($d->{type} eq 'qemu') {
1777 $class = 'PVE::QemuConfig';
1778 } else {
1779 die "unknown virtual guest type '$d->{type}'\n";
1780 }
1781
1782 my $conf = $class->load_config($vmid);
1783 return if !$templates && $class->is_template($conf);
1784 return if !$ha_managed && PVE::HA::Config::vm_is_ha_managed($vmid);
1785
1786 $res->{$vmid}->{conf} = $conf;
1787 $res->{$vmid}->{type} = $d->{type};
1788 $res->{$vmid}->{class} = $class;
1789 };
1790 warn $@ if $@;
1791 }
1792
1793 return $res;
1794 };
1795
1796 # return all VMs which should get started/stopped on power up/down
1797 my $get_start_stop_list = sub {
1798 my ($nodename, $autostart, $vmfilter) = @_;
1799
1800 # do not skip HA vms on force or if a specific VMID set is wanted
1801 my $include_ha_managed = defined($vmfilter) ? 1 : 0;
1802
1803 my $vmlist = $get_filtered_vmlist->($nodename, $vmfilter, undef, $include_ha_managed);
1804
1805 my $resList = {};
1806 foreach my $vmid (keys %$vmlist) {
1807 my $conf = $vmlist->{$vmid}->{conf};
1808 next if $autostart && !$conf->{onboot};
1809
1810 my $startup = $conf->{startup} ? PVE::JSONSchema::pve_parse_startup_order($conf->{startup}) : {};
1811 my $order = $startup->{order} = $startup->{order} // LONG_MAX;
1812
1813 $resList->{$order}->{$vmid} = $startup;
1814 $resList->{$order}->{$vmid}->{type} = $vmlist->{$vmid}->{type};
1815 }
1816
1817 return $resList;
1818 };
1819
1820 my $remove_locks_on_startup = sub {
1821 my ($nodename) = @_;
1822
1823 my $vmlist = &$get_filtered_vmlist($nodename, undef, undef, 1);
1824
1825 foreach my $vmid (keys %$vmlist) {
1826 my $conf = $vmlist->{$vmid}->{conf};
1827 my $class = $vmlist->{$vmid}->{class};
1828
1829 eval {
1830 if ($class->has_lock($conf, 'backup')) {
1831 $class->remove_lock($vmid, 'backup');
1832 my $msg = "removed left over backup lock from '$vmid'!";
1833 warn "$msg\n"; # prints to task log
1834 syslog('warning', $msg);
1835 }
1836 }; warn $@ if $@;
1837 }
1838 };
1839
1840 __PACKAGE__->register_method ({
1841 name => 'startall',
1842 path => 'startall',
1843 method => 'POST',
1844 protected => 1,
1845 permissions => {
1846 description => "The 'VM.PowerMgmt' permission is required on '/' or on '/vms/<ID>' for "
1847 ."each ID passed via the 'vms' parameter.",
1848 user => 'all',
1849 },
1850 proxyto => 'node',
1851 description => "Start all VMs and containers located on this node (by default only those with onboot=1).",
1852 parameters => {
1853 additionalProperties => 0,
1854 properties => {
1855 node => get_standard_option('pve-node'),
1856 force => {
1857 optional => 1,
1858 type => 'boolean',
1859 default => 'off',
1860 description => "Issue start command even if virtual guest have 'onboot' not set or set to off.",
1861 },
1862 vms => {
1863 description => "Only consider guests from this comma separated list of VMIDs.",
1864 type => 'string', format => 'pve-vmid-list',
1865 optional => 1,
1866 },
1867 },
1868 },
1869 returns => {
1870 type => 'string',
1871 },
1872 code => sub {
1873 my ($param) = @_;
1874
1875 my $rpcenv = PVE::RPCEnvironment::get();
1876 my $authuser = $rpcenv->get_user();
1877
1878 if (!$rpcenv->check($authuser, "/", [ 'VM.PowerMgmt' ], 1)) {
1879 my @vms = PVE::Tools::split_list($param->{vms});
1880 if (scalar(@vms) > 0) {
1881 $rpcenv->check($authuser, "/vms/$_", [ 'VM.PowerMgmt' ]) for @vms;
1882 } else {
1883 raise_perm_exc("/, VM.PowerMgmt");
1884 }
1885 }
1886
1887 my $nodename = $param->{node};
1888 $nodename = PVE::INotify::nodename() if $nodename eq 'localhost';
1889
1890 my $force = $param->{force};
1891
1892 my $code = sub {
1893 $rpcenv->{type} = 'priv'; # to start tasks in background
1894
1895 if (!PVE::Cluster::check_cfs_quorum(1)) {
1896 print "waiting for quorum ...\n";
1897 do {
1898 sleep(1);
1899 } while (!PVE::Cluster::check_cfs_quorum(1));
1900 print "got quorum\n";
1901 }
1902
1903 eval { # remove backup locks, but avoid running into a scheduled backup job
1904 PVE::Tools::lock_file('/var/run/vzdump.lock', 10, $remove_locks_on_startup, $nodename);
1905 };
1906 warn $@ if $@;
1907
1908 my $autostart = $force ? undef : 1;
1909 my $startList = $get_start_stop_list->($nodename, $autostart, $param->{vms});
1910
1911 # Note: use numeric sorting with <=>
1912 for my $order (sort {$a <=> $b} keys %$startList) {
1913 my $vmlist = $startList->{$order};
1914
1915 for my $vmid (sort {$a <=> $b} keys %$vmlist) {
1916 my $d = $vmlist->{$vmid};
1917
1918 PVE::Cluster::check_cfs_quorum(); # abort when we loose quorum
1919
1920 eval {
1921 my $default_delay = 0;
1922 my $upid;
1923
1924 if ($d->{type} eq 'lxc') {
1925 return if PVE::LXC::check_running($vmid);
1926 print STDERR "Starting CT $vmid\n";
1927 $upid = PVE::API2::LXC::Status->vm_start({node => $nodename, vmid => $vmid });
1928 } elsif ($d->{type} eq 'qemu') {
1929 $default_delay = 3; # to reduce load
1930 return if PVE::QemuServer::check_running($vmid, 1);
1931 print STDERR "Starting VM $vmid\n";
1932 $upid = PVE::API2::Qemu->vm_start({node => $nodename, vmid => $vmid });
1933 } else {
1934 die "unknown VM type '$d->{type}'\n";
1935 }
1936
1937 my $task = PVE::Tools::upid_decode($upid);
1938 while (PVE::ProcFSTools::check_process_running($task->{pid})) {
1939 sleep(1);
1940 }
1941
1942 my $status = PVE::Tools::upid_read_status($upid);
1943 if (!PVE::Tools::upid_status_is_error($status)) {
1944 # use default delay to reduce load
1945 my $delay = defined($d->{up}) ? int($d->{up}) : $default_delay;
1946 if ($delay > 0) {
1947 print STDERR "Waiting for $delay seconds (startup delay)\n" if $d->{up};
1948 for (my $i = 0; $i < $delay; $i++) {
1949 sleep(1);
1950 }
1951 }
1952 } else {
1953 my $rendered_type = $d->{type} eq 'lxc' ? 'CT' : 'VM';
1954 print STDERR "Starting $rendered_type $vmid failed: $status\n";
1955 }
1956 };
1957 warn $@ if $@;
1958 }
1959 }
1960 return;
1961 };
1962
1963 return $rpcenv->fork_worker('startall', undef, $authuser, $code);
1964 }});
1965
1966 my $create_stop_worker = sub {
1967 my ($nodename, $type, $vmid, $timeout, $force_stop) = @_;
1968
1969 if ($type eq 'lxc') {
1970 return if !PVE::LXC::check_running($vmid);
1971 print STDERR "Stopping CT $vmid (timeout = $timeout seconds)\n";
1972 return PVE::API2::LXC::Status->vm_shutdown(
1973 { node => $nodename, vmid => $vmid, timeout => $timeout, forceStop => $force_stop }
1974 );
1975 } elsif ($type eq 'qemu') {
1976 return if !PVE::QemuServer::check_running($vmid, 1);
1977 print STDERR "Stopping VM $vmid (timeout = $timeout seconds)\n";
1978 return PVE::API2::Qemu->vm_shutdown(
1979 { node => $nodename, vmid => $vmid, timeout => $timeout, forceStop => $force_stop }
1980 );
1981 } else {
1982 die "unknown VM type '$type'\n";
1983 }
1984 };
1985
1986 __PACKAGE__->register_method ({
1987 name => 'stopall',
1988 path => 'stopall',
1989 method => 'POST',
1990 protected => 1,
1991 permissions => {
1992 description => "The 'VM.PowerMgmt' permission is required on '/' or on '/vms/<ID>' for "
1993 ."each ID passed via the 'vms' parameter.",
1994 user => 'all',
1995 },
1996 proxyto => 'node',
1997 description => "Stop all VMs and Containers.",
1998 parameters => {
1999 additionalProperties => 0,
2000 properties => {
2001 node => get_standard_option('pve-node'),
2002 vms => {
2003 description => "Only consider Guests with these IDs.",
2004 type => 'string', format => 'pve-vmid-list',
2005 optional => 1,
2006 },
2007 'force-stop' => {
2008 description => 'Force a hard-stop after the timeout.',
2009 type => 'boolean',
2010 default => 1,
2011 optional => 1,
2012 },
2013 'timeout' => {
2014 description => 'Timeout for each guest shutdown task. Depending on `force-stop`,'
2015 .' the shutdown gets then simply aborted or a hard-stop is forced.',
2016 type => 'integer',
2017 optional => 1,
2018 default => 180,
2019 minimum => 0,
2020 maximum => 2 * 3600, # mostly arbitrary, but we do not want to high timeouts
2021 },
2022 },
2023 },
2024 returns => {
2025 type => 'string',
2026 },
2027 code => sub {
2028 my ($param) = @_;
2029
2030 my $rpcenv = PVE::RPCEnvironment::get();
2031 my $authuser = $rpcenv->get_user();
2032
2033 if (!$rpcenv->check($authuser, "/", [ 'VM.PowerMgmt' ], 1)) {
2034 my @vms = PVE::Tools::split_list($param->{vms});
2035 if (scalar(@vms) > 0) {
2036 $rpcenv->check($authuser, "/vms/$_", [ 'VM.PowerMgmt' ]) for @vms;
2037 } else {
2038 raise_perm_exc("/, VM.PowerMgmt");
2039 }
2040 }
2041
2042 my $nodename = $param->{node};
2043 $nodename = PVE::INotify::nodename() if $nodename eq 'localhost';
2044
2045 my $code = sub {
2046
2047 $rpcenv->{type} = 'priv'; # to start tasks in background
2048
2049 my $stopList = $get_start_stop_list->($nodename, undef, $param->{vms});
2050
2051 my $cpuinfo = PVE::ProcFSTools::read_cpuinfo();
2052 my $datacenterconfig = cfs_read_file('datacenter.cfg');
2053 # if not set by user spawn max cpu count number of workers
2054 my $maxWorkers = $datacenterconfig->{max_workers} || $cpuinfo->{cpus};
2055
2056 for my $order (sort {$b <=> $a} keys %$stopList) {
2057 my $vmlist = $stopList->{$order};
2058 my $workers = {};
2059
2060 my $finish_worker = sub {
2061 my $pid = shift;
2062 my $worker = delete $workers->{$pid} || return;
2063
2064 syslog('info', "end task $worker->{upid}");
2065 };
2066
2067 for my $vmid (sort {$b <=> $a} keys %$vmlist) {
2068 my $d = $vmlist->{$vmid};
2069 my $timeout = int($d->{down} // $param->{timeout} // 180);
2070 my $upid = eval {
2071 $create_stop_worker->(
2072 $nodename, $d->{type}, $vmid, $timeout, $param->{'force-stop'} // 1)
2073 };
2074 warn $@ if $@;
2075 next if !$upid;
2076
2077 my $task = PVE::Tools::upid_decode($upid, 1);
2078 next if !$task;
2079
2080 my $pid = $task->{pid};
2081
2082 $workers->{$pid} = { type => $d->{type}, upid => $upid, vmid => $vmid };
2083 while (scalar(keys %$workers) >= $maxWorkers) {
2084 foreach my $p (keys %$workers) {
2085 if (!PVE::ProcFSTools::check_process_running($p)) {
2086 $finish_worker->($p);
2087 }
2088 }
2089 sleep(1);
2090 }
2091 }
2092 while (scalar(keys %$workers)) {
2093 for my $p (keys %$workers) {
2094 if (!PVE::ProcFSTools::check_process_running($p)) {
2095 $finish_worker->($p);
2096 }
2097 }
2098 sleep(1);
2099 }
2100 }
2101
2102 syslog('info', "all VMs and CTs stopped");
2103
2104 return;
2105 };
2106
2107 return $rpcenv->fork_worker('stopall', undef, $authuser, $code);
2108 }});
2109
2110 my $create_suspend_worker = sub {
2111 my ($nodename, $vmid) = @_;
2112 if (!PVE::QemuServer::check_running($vmid, 1)) {
2113 print "VM $vmid not running, skipping suspension\n";
2114 return;
2115 }
2116 print STDERR "Suspending VM $vmid\n";
2117 return PVE::API2::Qemu->vm_suspend(
2118 { node => $nodename, vmid => $vmid, todisk => 1 }
2119 );
2120 };
2121
2122 __PACKAGE__->register_method ({
2123 name => 'suspendall',
2124 path => 'suspendall',
2125 method => 'POST',
2126 protected => 1,
2127 permissions => {
2128 description => "The 'VM.PowerMgmt' permission is required on '/' or on '/vms/<ID>' for each"
2129 ." ID passed via the 'vms' parameter. Additionally, you need 'VM.Config.Disk' on the"
2130 ." '/vms/{vmid}' path and 'Datastore.AllocateSpace' for the configured state-storage(s)",
2131 user => 'all',
2132 },
2133 proxyto => 'node',
2134 description => "Suspend all VMs.",
2135 parameters => {
2136 additionalProperties => 0,
2137 properties => {
2138 node => get_standard_option('pve-node'),
2139 vms => {
2140 description => "Only consider Guests with these IDs.",
2141 type => 'string', format => 'pve-vmid-list',
2142 optional => 1,
2143 },
2144 },
2145 },
2146 returns => {
2147 type => 'string',
2148 },
2149 code => sub {
2150 my ($param) = @_;
2151
2152 my $rpcenv = PVE::RPCEnvironment::get();
2153 my $authuser = $rpcenv->get_user();
2154
2155 # we cannot really check access to the state-storage here, that's happening per worker.
2156 if (!$rpcenv->check($authuser, "/", [ 'VM.PowerMgmt', 'VM.Config.Disk' ], 1)) {
2157 my @vms = PVE::Tools::split_list($param->{vms});
2158 if (scalar(@vms) > 0) {
2159 $rpcenv->check($authuser, "/vms/$_", [ 'VM.PowerMgmt' ]) for @vms;
2160 } else {
2161 raise_perm_exc("/, VM.PowerMgmt && VM.Config.Disk");
2162 }
2163 }
2164
2165 my $nodename = $param->{node};
2166 $nodename = PVE::INotify::nodename() if $nodename eq 'localhost';
2167
2168 my $code = sub {
2169
2170 $rpcenv->{type} = 'priv'; # to start tasks in background
2171
2172 my $toSuspendList = $get_start_stop_list->($nodename, undef, $param->{vms});
2173
2174 my $cpuinfo = PVE::ProcFSTools::read_cpuinfo();
2175 my $datacenterconfig = cfs_read_file('datacenter.cfg');
2176 # if not set by user spawn max cpu count number of workers
2177 my $maxWorkers = $datacenterconfig->{max_workers} || $cpuinfo->{cpus};
2178
2179 for my $order (sort {$b <=> $a} keys %$toSuspendList) {
2180 my $vmlist = $toSuspendList->{$order};
2181 my $workers = {};
2182
2183 my $finish_worker = sub {
2184 my $pid = shift;
2185 my $worker = delete $workers->{$pid} || return;
2186
2187 syslog('info', "end task $worker->{upid}");
2188 };
2189
2190 for my $vmid (sort {$b <=> $a} keys %$vmlist) {
2191 my $d = $vmlist->{$vmid};
2192 if ($d->{type} ne 'qemu') {
2193 log_warn("skipping $vmid, only VMs can be suspended");
2194 next;
2195 }
2196 my $upid = eval {
2197 $create_suspend_worker->($nodename, $vmid)
2198 };
2199 warn $@ if $@;
2200 next if !$upid;
2201
2202 my $task = PVE::Tools::upid_decode($upid, 1);
2203 next if !$task;
2204
2205 my $pid = $task->{pid};
2206 $workers->{$pid} = { type => $d->{type}, upid => $upid, vmid => $vmid };
2207
2208 while (scalar(keys %$workers) >= $maxWorkers) {
2209 for my $p (keys %$workers) {
2210 if (!PVE::ProcFSTools::check_process_running($p)) {
2211 $finish_worker->($p);
2212 }
2213 }
2214 sleep(1);
2215 }
2216 }
2217 while (scalar(keys %$workers)) {
2218 for my $p (keys %$workers) {
2219 if (!PVE::ProcFSTools::check_process_running($p)) {
2220 $finish_worker->($p);
2221 }
2222 }
2223 sleep(1);
2224 }
2225 }
2226
2227 syslog('info', "all VMs suspended");
2228
2229 return;
2230 };
2231
2232 return $rpcenv->fork_worker('suspendall', undef, $authuser, $code);
2233 }});
2234
2235
2236 my $create_migrate_worker = sub {
2237 my ($nodename, $type, $vmid, $target, $with_local_disks) = @_;
2238
2239 my $upid;
2240 if ($type eq 'lxc') {
2241 my $online = PVE::LXC::check_running($vmid) ? 1 : 0;
2242 print STDERR "Migrating CT $vmid\n";
2243 $upid = PVE::API2::LXC->migrate_vm(
2244 { node => $nodename, vmid => $vmid, target => $target, restart => $online });
2245 } elsif ($type eq 'qemu') {
2246 print STDERR "Check VM $vmid: ";
2247 *STDERR->flush();
2248 my $online = PVE::QemuServer::check_running($vmid, 1) ? 1 : 0;
2249 my $preconditions = PVE::API2::Qemu->migrate_vm_precondition(
2250 {node => $nodename, vmid => $vmid, target => $target});
2251 my $invalidConditions = '';
2252 if ($online && !$with_local_disks && scalar @{$preconditions->{local_disks}}) {
2253 $invalidConditions .= "\n Has local disks: ";
2254 $invalidConditions .= join(', ', map { $_->{volid} } @{$preconditions->{local_disks}});
2255 }
2256
2257 if (@{$preconditions->{local_resources}}) {
2258 $invalidConditions .= "\n Has local resources: ";
2259 $invalidConditions .= join(', ', @{$preconditions->{local_resources}});
2260 }
2261
2262 if ($invalidConditions && $invalidConditions ne '') {
2263 print STDERR "skip VM $vmid - precondition check failed:";
2264 die "$invalidConditions\n";
2265 }
2266 print STDERR "precondition check passed\n";
2267 print STDERR "Migrating VM $vmid\n";
2268
2269 my $params = {
2270 node => $nodename,
2271 vmid => $vmid,
2272 target => $target,
2273 online => $online,
2274 };
2275 $params->{'with-local-disks'} = $with_local_disks if defined($with_local_disks);
2276
2277 $upid = PVE::API2::Qemu->migrate_vm($params);
2278 } else {
2279 die "unknown VM type '$type'\n";
2280 }
2281
2282 my $task = PVE::Tools::upid_decode($upid);
2283
2284 return $task->{pid};
2285 };
2286
2287 __PACKAGE__->register_method ({
2288 name => 'migrateall',
2289 path => 'migrateall',
2290 method => 'POST',
2291 proxyto => 'node',
2292 protected => 1,
2293 permissions => {
2294 description => "The 'VM.Migrate' permission is required on '/' or on '/vms/<ID>' for each "
2295 ."ID passed via the 'vms' parameter.",
2296 user => 'all',
2297 },
2298 description => "Migrate all VMs and Containers.",
2299 parameters => {
2300 additionalProperties => 0,
2301 properties => {
2302 node => get_standard_option('pve-node'),
2303 target => get_standard_option('pve-node', { description => "Target node." }),
2304 maxworkers => {
2305 description => "Maximal number of parallel migration job. If not set, uses"
2306 ."'max_workers' from datacenter.cfg. One of both must be set!",
2307 optional => 1,
2308 type => 'integer',
2309 minimum => 1
2310 },
2311 vms => {
2312 description => "Only consider Guests with these IDs.",
2313 type => 'string', format => 'pve-vmid-list',
2314 optional => 1,
2315 },
2316 "with-local-disks" => {
2317 type => 'boolean',
2318 description => "Enable live storage migration for local disk",
2319 optional => 1,
2320 },
2321 },
2322 },
2323 returns => {
2324 type => 'string',
2325 },
2326 code => sub {
2327 my ($param) = @_;
2328
2329 my $rpcenv = PVE::RPCEnvironment::get();
2330 my $authuser = $rpcenv->get_user();
2331
2332 if (!$rpcenv->check($authuser, "/", [ 'VM.Migrate' ], 1)) {
2333 my @vms = PVE::Tools::split_list($param->{vms});
2334 if (scalar(@vms) > 0) {
2335 $rpcenv->check($authuser, "/vms/$_", [ 'VM.Migrate' ]) for @vms;
2336 } else {
2337 raise_perm_exc("/, VM.Migrate");
2338 }
2339 }
2340
2341 my $nodename = $param->{node};
2342 $nodename = PVE::INotify::nodename() if $nodename eq 'localhost';
2343
2344 my $target = $param->{target};
2345 my $with_local_disks = $param->{'with-local-disks'};
2346 raise_param_exc({ target => "target is local node."}) if $target eq $nodename;
2347
2348 PVE::Cluster::check_cfs_quorum();
2349
2350 PVE::Cluster::check_node_exists($target);
2351
2352 my $datacenterconfig = cfs_read_file('datacenter.cfg');
2353 # prefer parameter over datacenter cfg settings
2354 my $maxWorkers = $param->{maxworkers} || $datacenterconfig->{max_workers} ||
2355 die "either 'maxworkers' parameter or max_workers in datacenter.cfg must be set!\n";
2356
2357 my $code = sub {
2358 $rpcenv->{type} = 'priv'; # to start tasks in background
2359
2360 my $vmlist = &$get_filtered_vmlist($nodename, $param->{vms}, 1, 1);
2361 if (!scalar(keys %$vmlist)) {
2362 warn "no virtual guests matched, nothing to do..\n";
2363 return;
2364 }
2365
2366 my $workers = {};
2367 my $workers_started = 0;
2368 foreach my $vmid (sort keys %$vmlist) {
2369 my $d = $vmlist->{$vmid};
2370 my $pid;
2371 eval { $pid = &$create_migrate_worker($nodename, $d->{type}, $vmid, $target, $with_local_disks); };
2372 warn $@ if $@;
2373 next if !$pid;
2374
2375 $workers_started++;
2376 $workers->{$pid} = 1;
2377 while (scalar(keys %$workers) >= $maxWorkers) {
2378 foreach my $p (keys %$workers) {
2379 if (!PVE::ProcFSTools::check_process_running($p)) {
2380 delete $workers->{$p};
2381 }
2382 }
2383 sleep(1);
2384 }
2385 }
2386 while (scalar(keys %$workers)) {
2387 foreach my $p (keys %$workers) {
2388 # FIXME: what about PID re-use ?!?!
2389 if (!PVE::ProcFSTools::check_process_running($p)) {
2390 delete $workers->{$p};
2391 }
2392 }
2393 sleep(1);
2394 }
2395 if ($workers_started <= 0) {
2396 die "no migrations worker started...\n";
2397 }
2398 print STDERR "All jobs finished, used $workers_started workers in total.\n";
2399 return;
2400 };
2401
2402 return $rpcenv->fork_worker('migrateall', undef, $authuser, $code);
2403
2404 }});
2405
2406 __PACKAGE__->register_method ({
2407 name => 'get_etc_hosts',
2408 path => 'hosts',
2409 method => 'GET',
2410 proxyto => 'node',
2411 protected => 1,
2412 permissions => {
2413 check => ['perm', '/', [ 'Sys.Audit' ]],
2414 },
2415 description => "Get the content of /etc/hosts.",
2416 parameters => {
2417 additionalProperties => 0,
2418 properties => {
2419 node => get_standard_option('pve-node'),
2420 },
2421 },
2422 returns => {
2423 type => 'object',
2424 properties => {
2425 digest => get_standard_option('pve-config-digest'),
2426 data => {
2427 type => 'string',
2428 description => 'The content of /etc/hosts.'
2429 },
2430 },
2431 },
2432 code => sub {
2433 my ($param) = @_;
2434
2435 return PVE::INotify::read_file('etchosts');
2436
2437 }});
2438
2439 __PACKAGE__->register_method ({
2440 name => 'write_etc_hosts',
2441 path => 'hosts',
2442 method => 'POST',
2443 proxyto => 'node',
2444 protected => 1,
2445 permissions => {
2446 check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
2447 },
2448 description => "Write /etc/hosts.",
2449 parameters => {
2450 additionalProperties => 0,
2451 properties => {
2452 node => get_standard_option('pve-node'),
2453 digest => get_standard_option('pve-config-digest'),
2454 data => {
2455 type => 'string',
2456 description => 'The target content of /etc/hosts.'
2457 },
2458 },
2459 },
2460 returns => {
2461 type => 'null',
2462 },
2463 code => sub {
2464 my ($param) = @_;
2465
2466 PVE::Tools::lock_file('/var/lock/pve-etchosts.lck', undef, sub {
2467 if ($param->{digest}) {
2468 my $hosts = PVE::INotify::read_file('etchosts');
2469 PVE::Tools::assert_if_modified($hosts->{digest}, $param->{digest});
2470 }
2471 PVE::INotify::write_file('etchosts', $param->{data});
2472 });
2473 die $@ if $@;
2474
2475 return;
2476 }});
2477
2478 # bash completion helper
2479
2480 sub complete_templet_repo {
2481 my ($cmdname, $pname, $cvalue) = @_;
2482
2483 my $repo = PVE::APLInfo::load_data();
2484 my $res = [];
2485 foreach my $templ (keys %{$repo->{all}}) {
2486 next if $templ !~ m/^$cvalue/;
2487 push @$res, $templ;
2488 }
2489
2490 return $res;
2491 }
2492
2493 package PVE::API2::Nodes;
2494
2495 use strict;
2496 use warnings;
2497
2498 use PVE::SafeSyslog;
2499 use PVE::Cluster;
2500 use PVE::RESTHandler;
2501 use PVE::RPCEnvironment;
2502 use PVE::API2Tools;
2503 use PVE::JSONSchema qw(get_standard_option);
2504
2505 use base qw(PVE::RESTHandler);
2506
2507 __PACKAGE__->register_method ({
2508 subclass => "PVE::API2::Nodes::Nodeinfo",
2509 path => '{node}',
2510 });
2511
2512 __PACKAGE__->register_method ({
2513 name => 'index',
2514 path => '',
2515 method => 'GET',
2516 permissions => { user => 'all' },
2517 description => "Cluster node index.",
2518 parameters => {
2519 additionalProperties => 0,
2520 properties => {},
2521 },
2522 returns => {
2523 type => 'array',
2524 items => {
2525 type => "object",
2526 properties => {
2527 node => get_standard_option('pve-node'),
2528 status => {
2529 description => "Node status.",
2530 type => 'string',
2531 enum => ['unknown', 'online', 'offline'],
2532 },
2533 cpu => {
2534 description => "CPU utilization.",
2535 type => 'number',
2536 optional => 1,
2537 renderer => 'fraction_as_percentage',
2538 },
2539 maxcpu => {
2540 description => "Number of available CPUs.",
2541 type => 'integer',
2542 optional => 1,
2543 },
2544 mem => {
2545 description => "Used memory in bytes.",
2546 type => 'integer',
2547 optional => 1,
2548 renderer => 'bytes',
2549 },
2550 maxmem => {
2551 description => "Number of available memory in bytes.",
2552 type => 'integer',
2553 optional => 1,
2554 renderer => 'bytes',
2555 },
2556 level => {
2557 description => "Support level.",
2558 type => 'string',
2559 optional => 1,
2560 },
2561 uptime => {
2562 description => "Node uptime in seconds.",
2563 type => 'integer',
2564 optional => 1,
2565 renderer => 'duration',
2566 },
2567 ssl_fingerprint => {
2568 description => "The SSL fingerprint for the node certificate.",
2569 type => 'string',
2570 optional => 1,
2571 },
2572 },
2573 },
2574 links => [ { rel => 'child', href => "{node}" } ],
2575 },
2576 code => sub {
2577 my ($param) = @_;
2578
2579 my $rpcenv = PVE::RPCEnvironment::get();
2580 my $authuser = $rpcenv->get_user();
2581
2582 my $clinfo = PVE::Cluster::get_clinfo();
2583 my $res = [];
2584
2585 my $nodelist = PVE::Cluster::get_nodelist();
2586 my $members = PVE::Cluster::get_members();
2587 my $rrd = PVE::Cluster::rrd_dump();
2588
2589 foreach my $node (@$nodelist) {
2590 my $can_audit = $rpcenv->check($authuser, "/nodes/$node", [ 'Sys.Audit' ], 1);
2591 my $entry = PVE::API2Tools::extract_node_stats($node, $members, $rrd, !$can_audit);
2592
2593 $entry->{ssl_fingerprint} = eval { PVE::Cluster::get_node_fingerprint($node) };
2594 warn "$@" if $@;
2595
2596 push @$res, $entry;
2597 }
2598
2599 return $res;
2600 }});
2601
2602 1;