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