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