]> git.proxmox.com Git - pmg-api.git/blob - src/PMG/API2/Nodes.pm
873434b6d55ebd46063fda71d98158c51d63f6b9
[pmg-api.git] / src / PMG / API2 / Nodes.pm
1 package PMG::API2::NodeInfo;
2
3 use strict;
4 use warnings;
5 use Time::Local qw(timegm_nocheck);
6 use Filesys::Df;
7 use Data::Dumper;
8
9 use PVE::Exception qw(raise_perm_exc);
10 use PVE::INotify;
11 use PVE::RESTHandler;
12 use PVE::JSONSchema qw(get_standard_option);
13 use PMG::RESTEnvironment;
14 use PVE::SafeSyslog;
15 use PVE::ProcFSTools;
16
17 use PMG::pmgcfg;
18 use PMG::Ticket;
19 use PMG::Report;
20 use PMG::API2::Subscription;
21 use PMG::API2::APT;
22 use PMG::API2::Tasks;
23 use PMG::API2::Services;
24 use PMG::API2::Network;
25 use PMG::API2::ClamAV;
26 use PMG::API2::SpamAssassin;
27 use PMG::API2::Postfix;
28 use PMG::API2::MailTracker;
29 use PMG::API2::Backup;
30 use PMG::API2::PBS::Job;
31 use PMG::API2::Certificates;
32 use PMG::API2::NodeConfig;
33
34 use base qw(PVE::RESTHandler);
35
36 __PACKAGE__->register_method ({
37 subclass => "PMG::API2::Postfix",
38 path => 'postfix',
39 });
40
41 __PACKAGE__->register_method ({
42 subclass => "PMG::API2::ClamAV",
43 path => 'clamav',
44 });
45
46 __PACKAGE__->register_method ({
47 subclass => "PMG::API2::SpamAssassin",
48 path => 'spamassassin',
49 });
50
51 __PACKAGE__->register_method ({
52 subclass => "PMG::API2::Network",
53 path => 'network',
54 });
55
56 __PACKAGE__->register_method ({
57 subclass => "PMG::API2::Tasks",
58 path => 'tasks',
59 });
60
61 __PACKAGE__->register_method ({
62 subclass => "PMG::API2::Services",
63 path => 'services',
64 });
65
66 __PACKAGE__->register_method ({
67 subclass => "PMG::API2::Subscription",
68 path => 'subscription',
69 });
70
71 __PACKAGE__->register_method ({
72 subclass => "PMG::API2::APT",
73 path => 'apt',
74 });
75
76 __PACKAGE__->register_method ({
77 subclass => "PMG::API2::MailTracker",
78 path => 'tracker',
79 });
80
81 __PACKAGE__->register_method ({
82 subclass => "PMG::API2::Backup",
83 path => 'backup',
84 });
85
86 __PACKAGE__->register_method ({
87 subclass => "PMG::API2::PBS::Job",
88 path => 'pbs',
89 });
90
91 __PACKAGE__->register_method ({
92 subclass => "PMG::API2::Certificates",
93 path => 'certificates',
94 });
95
96 __PACKAGE__->register_method ({
97 subclass => "PMG::API2::NodeConfig",
98 path => 'config',
99 });
100
101 __PACKAGE__->register_method ({
102 name => 'index',
103 path => '',
104 method => 'GET',
105 permissions => { user => 'all' },
106 description => "Node index.",
107 parameters => {
108 additionalProperties => 0,
109 properties => {
110 node => get_standard_option('pve-node'),
111 },
112 },
113 returns => {
114 type => 'array',
115 items => {
116 type => "object",
117 properties => {},
118 },
119 links => [ { rel => 'child', href => "{name}" } ],
120 },
121 code => sub {
122 my ($param) = @_;
123
124 my $result = [
125 { name => 'apt' },
126 { name => 'backup' },
127 { name => 'pbs' },
128 { name => 'clamav' },
129 { name => 'spamassassin' },
130 { name => 'postfix' },
131 { name => 'services' },
132 { name => 'syslog' },
133 { name => 'journal' },
134 { name => 'tasks' },
135 { name => 'tracker' },
136 { name => 'time' },
137 { name => 'report' },
138 { name => 'status' },
139 { name => 'subscription' },
140 { name => 'termproxy' },
141 { name => 'rrddata' },
142 { name => 'certificates' },
143 { name => 'config' },
144 ];
145
146 return $result;
147 }});
148
149 __PACKAGE__->register_method({
150 name => 'report',
151 path => 'report',
152 method => 'GET',
153 protected => 1,
154 proxyto => 'node',
155 permissions => { check => [ 'admin', 'audit' ] },
156 description => "Gather various system information about a node",
157 parameters => {
158 additionalProperties => 0,
159 properties => {
160 node => get_standard_option('pve-node'),
161 },
162 },
163 returns => {
164 type => 'string',
165 },
166 code => sub {
167 return PMG::Report::generate();
168 }});
169
170 __PACKAGE__->register_method({
171 name => 'rrddata',
172 path => 'rrddata',
173 method => 'GET',
174 protected => 1, # fixme: can we avoid that?
175 proxyto => 'node',
176 permissions => { check => [ 'admin', 'audit' ] },
177 description => "Read node RRD statistics",
178 parameters => {
179 additionalProperties => 0,
180 properties => {
181 node => get_standard_option('pve-node'),
182 timeframe => {
183 description => "Specify the time frame you are interested in.",
184 type => 'string',
185 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
186 },
187 cf => {
188 description => "The RRD consolidation function",
189 type => 'string',
190 enum => [ 'AVERAGE', 'MAX' ],
191 optional => 1,
192 },
193 },
194 },
195 returns => {
196 type => "array",
197 items => {
198 type => "object",
199 properties => {},
200 },
201 },
202 code => sub {
203 my ($param) = @_;
204
205 return PMG::Utils::create_rrd_data(
206 "pmg-node-v1.rrd", $param->{timeframe}, $param->{cf});
207 }});
208
209
210 __PACKAGE__->register_method({
211 name => 'syslog',
212 path => 'syslog',
213 method => 'GET',
214 description => "Read system log",
215 proxyto => 'node',
216 protected => 1,
217 permissions => { check => [ 'admin', 'audit' ] },
218 parameters => {
219 additionalProperties => 0,
220 properties => {
221 node => get_standard_option('pve-node'),
222 start => {
223 type => 'integer',
224 minimum => 0,
225 optional => 1,
226 },
227 limit => {
228 type => 'integer',
229 minimum => 0,
230 optional => 1,
231 },
232 since => {
233 type => 'string',
234 pattern => '^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}(:\d{2})?)?$',
235 description => "Display all log since this date-time string.",
236 optional => 1,
237 },
238 'until' => {
239 type => 'string',
240 pattern => '^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}(:\d{2})?)?$',
241 description => "Display all log until this date-time string.",
242 optional => 1,
243 },
244 service => {
245 description => "Service ID",
246 type => 'string',
247 maxLength => 128,
248 optional => 1,
249 },
250 },
251 },
252 returns => {
253 type => 'array',
254 items => {
255 type => "object",
256 properties => {
257 n => {
258 description=> "Line number",
259 type=> 'integer',
260 },
261 t => {
262 description=> "Line text",
263 type => 'string',
264 }
265 }
266 }
267 },
268 code => sub {
269 my ($param) = @_;
270
271 my $restenv = PMG::RESTEnvironment->get();
272
273 my $service = $param->{service};
274 $service = PMG::Utils::lookup_real_service_name($service)
275 if $service;
276
277 my ($count, $lines) = PVE::Tools::dump_journal(
278 $param->{start}, $param->{limit},
279 $param->{since}, $param->{'until'}, $service);
280
281 $restenv->set_result_attrib('total', $count);
282
283 return $lines;
284 }});
285
286 __PACKAGE__->register_method({
287 name => 'journal',
288 path => 'journal',
289 method => 'GET',
290 description => "Read Journal",
291 proxyto => 'node',
292 permissions => { check => [ 'admin', 'audit' ] },
293 protected => 1,
294 parameters => {
295 additionalProperties => 0,
296 properties => {
297 node => get_standard_option('pve-node'),
298 since => {
299 description => "Display all log since this UNIX epoch. Conflicts with 'startcursor'.",
300 type => 'integer',
301 minimum => 0,
302 optional => 1,
303 },
304 until => {
305 description => "Display all log until this UNIX epoch. Conflicts with 'endcursor'.",
306 type => 'integer',
307 minimum => 0,
308 optional => 1,
309 },
310 lastentries => {
311 description => "Limit to the last X lines. Conflicts with a range.",
312 type => 'integer',
313 minimum => 0,
314 optional => 1,
315 },
316 startcursor => {
317 description => "Start after the given Cursor. Conflicts with 'since'.",
318 type => 'string',
319 optional => 1,
320 },
321 endcursor => {
322 description => "End before the given Cursor. Conflicts with 'until'.",
323 type => 'string',
324 optional => 1,
325 },
326 },
327 },
328 returns => {
329 type => 'array',
330 items => {
331 type => "string",
332 }
333 },
334 code => sub {
335 my ($param) = @_;
336
337 my $cmd = ["/usr/bin/mini-journalreader", "-j"];
338 push @$cmd, '-n', $param->{lastentries} if $param->{lastentries};
339 push @$cmd, '-b', $param->{since} if $param->{since};
340 push @$cmd, '-e', $param->{until} if $param->{until};
341 push @$cmd, '-f', PVE::Tools::shellquote($param->{startcursor}) if $param->{startcursor};
342 push @$cmd, '-t', PVE::Tools::shellquote($param->{endcursor}) if $param->{endcursor};
343 push @$cmd, ' | gzip ';
344
345 open(my $fh, "-|", join(' ', @$cmd))
346 or die "could not start mini-journalreader";
347
348 return {
349 download => {
350 fh => $fh,
351 stream => 1,
352 'content-type' => 'application/json',
353 'content-encoding' => 'gzip',
354 },
355 },
356 }});
357
358 my $shell_cmd_map = {
359 'login' => {
360 cmd => [ '/bin/login', '-f', 'root' ],
361 },
362 'upgrade' => {
363 cmd => [ 'pmgupgrade', '--shell' ],
364 },
365 };
366
367 sub get_shell_command {
368 my ($user, $shellcmd, $args) = @_;
369
370 my $cmd;
371 if ($user eq 'root@pam') {
372 if (defined($shellcmd) && exists($shell_cmd_map->{$shellcmd})) {
373 my $def = $shell_cmd_map->{$shellcmd};
374 $cmd = [ @{$def->{cmd}} ]; # clone
375 if (defined($args) && $def->{allow_args}) {
376 push @$cmd, split("\0", $args);
377 }
378 } else {
379 $cmd = [ '/bin/login', '-f', 'root' ];
380 }
381 } else {
382 # non-root must always login for now, we do not have a superuser role!
383 $cmd = [ '/bin/login' ];
384 }
385 return $cmd;
386 }
387
388 __PACKAGE__->register_method ({
389 name => 'termproxy',
390 path => 'termproxy',
391 method => 'POST',
392 permissions => { check => [ 'admin' ] },
393 protected => 1,
394 description => "Creates a Terminal proxy.",
395 parameters => {
396 additionalProperties => 0,
397 properties => {
398 node => get_standard_option('pve-node'),
399 cmd => {
400 type => 'string',
401 description => "Run specific command or default to login.",
402 enum => [sort keys %$shell_cmd_map],
403 optional => 1,
404 default => 'login',
405 },
406 'cmd-opts' => {
407 type => 'string',
408 description => "Add parameters to a command. Encoded as null terminated strings.",
409 requires => 'cmd',
410 optional => 1,
411 default => '',
412 },
413 },
414 },
415 returns => {
416 additionalProperties => 0,
417 properties => {
418 user => { type => 'string' },
419 ticket => { type => 'string' },
420 port => { type => 'integer' },
421 upid => { type => 'string' },
422 },
423 },
424 code => sub {
425 my ($param) = @_;
426
427 my $node = $param->{node};
428
429 if ($node ne PVE::INotify::nodename()) {
430 die "termproxy to remote node not implemented";
431 }
432
433 my $authpath = "/nodes/$node";
434
435 my $restenv = PMG::RESTEnvironment->get();
436 my $user = $restenv->get_user();
437
438 if (defined($param->{cmd}) && $param->{cmd} eq 'upgrade' && $user ne 'root@pam') {
439 raise_perm_exc('user != root@pam');
440 }
441
442 my $ticket = PMG::Ticket::assemble_vnc_ticket($user, $authpath);
443
444 my $family = PVE::Tools::get_host_address_family($node);
445 my $port = PVE::Tools::next_vnc_port($family);
446
447 my $shcmd = get_shell_command($user, $param->{cmd}, $param->{'cmd-opts'});
448
449 my $cmd = ['/usr/bin/termproxy', $port, '--path', $authpath, '--', @$shcmd];
450
451 my $realcmd = sub {
452 my $upid = shift;
453
454 syslog ('info', "starting termproxy $upid\n");
455
456 my $cmdstr = join (' ', @$cmd);
457 syslog ('info', "launch command: $cmdstr");
458
459 PVE::Tools::run_command($cmd);
460
461 return;
462 };
463
464 my $upid = $restenv->fork_worker('termproxy', "", $user, $realcmd);
465
466 PVE::Tools::wait_for_vnc_port($port);
467
468 return {
469 user => $user,
470 ticket => $ticket,
471 port => $port,
472 upid => $upid,
473 };
474 }});
475
476 __PACKAGE__->register_method({
477 name => 'vncwebsocket',
478 path => 'vncwebsocket',
479 method => 'GET',
480 permissions => { check => [ 'admin' ] },
481 description => "Opens a weksocket for VNC traffic.",
482 parameters => {
483 additionalProperties => 0,
484 properties => {
485 node => get_standard_option('pve-node'),
486 vncticket => {
487 description => "Ticket from previous call to vncproxy.",
488 type => 'string',
489 maxLength => 512,
490 },
491 port => {
492 description => "Port number returned by previous vncproxy call.",
493 type => 'integer',
494 minimum => 5900,
495 maximum => 5999,
496 },
497 },
498 },
499 returns => {
500 type => "object",
501 properties => {
502 port => { type => 'string' },
503 },
504 },
505 code => sub {
506 my ($param) = @_;
507
508 my $authpath = "/nodes/$param->{node}";
509
510 my $restenv = PMG::RESTEnvironment->get();
511 my $user = $restenv->get_user();
512
513 PMG::Ticket::verify_vnc_ticket($param->{vncticket}, $user, $authpath);
514
515 my $port = $param->{port};
516
517 return { port => $port };
518 }});
519
520 __PACKAGE__->register_method({
521 name => 'dns',
522 path => 'dns',
523 method => 'GET',
524 description => "Read DNS settings.",
525 proxyto => 'node',
526 permissions => { check => [ 'admin', 'audit' ] },
527 parameters => {
528 additionalProperties => 0,
529 properties => {
530 node => get_standard_option('pve-node'),
531 },
532 },
533 returns => {
534 type => "object",
535 additionalProperties => 0,
536 properties => {
537 search => {
538 description => "Search domain for host-name lookup.",
539 type => 'string',
540 optional => 1,
541 },
542 dns1 => {
543 description => 'First name server IP address.',
544 type => 'string',
545 optional => 1,
546 },
547 dns2 => {
548 description => 'Second name server IP address.',
549 type => 'string',
550 optional => 1,
551 },
552 dns3 => {
553 description => 'Third name server IP address.',
554 type => 'string',
555 optional => 1,
556 },
557 },
558 },
559 code => sub {
560 my ($param) = @_;
561
562 my $res = PVE::INotify::read_file('resolvconf');
563
564 return $res;
565 }});
566
567 __PACKAGE__->register_method({
568 name => 'update_dns',
569 path => 'dns',
570 method => 'PUT',
571 description => "Write DNS settings.",
572 proxyto => 'node',
573 protected => 1,
574 parameters => {
575 additionalProperties => 0,
576 properties => {
577 node => get_standard_option('pve-node'),
578 search => {
579 description => "Search domain for host-name lookup.",
580 type => 'string',
581 },
582 dns1 => {
583 description => 'First name server IP address.',
584 type => 'string', format => 'ip',
585 optional => 1,
586 },
587 dns2 => {
588 description => 'Second name server IP address.',
589 type => 'string', format => 'ip',
590 optional => 1,
591 },
592 dns3 => {
593 description => 'Third name server IP address.',
594 type => 'string', format => 'ip',
595 optional => 1,
596 },
597 },
598 },
599 returns => { type => "null" },
600 code => sub {
601 my ($param) = @_;
602
603 PVE::INotify::update_file('resolvconf', $param);
604
605 return undef;
606 }});
607
608
609 __PACKAGE__->register_method({
610 name => 'time',
611 path => 'time',
612 method => 'GET',
613 description => "Read server time and time zone settings.",
614 proxyto => 'node',
615 permissions => { check => [ 'admin', 'audit' ] },
616 parameters => {
617 additionalProperties => 0,
618 properties => {
619 node => get_standard_option('pve-node'),
620 },
621 },
622 returns => {
623 type => "object",
624 additionalProperties => 0,
625 properties => {
626 timezone => {
627 description => "Time zone",
628 type => 'string',
629 },
630 time => {
631 description => "Seconds since 1970-01-01 00:00:00 UTC.",
632 type => 'integer',
633 minimum => 1297163644,
634 },
635 localtime => {
636 description => "Seconds since 1970-01-01 00:00:00 (local time)",
637 type => 'integer',
638 minimum => 1297163644,
639 },
640 },
641 },
642 code => sub {
643 my ($param) = @_;
644
645 my $ctime = time();
646 my $ltime = timegm_nocheck(localtime($ctime));
647 my $res = {
648 timezone => PVE::INotify::read_file('timezone'),
649 time => time(),
650 localtime => $ltime,
651 };
652
653 return $res;
654 }});
655
656 __PACKAGE__->register_method({
657 name => 'set_timezone',
658 path => 'time',
659 method => 'PUT',
660 description => "Set time zone.",
661 proxyto => 'node',
662 protected => 1,
663 parameters => {
664 additionalProperties => 0,
665 properties => {
666 node => get_standard_option('pve-node'),
667 timezone => {
668 description => "Time zone. The file '/usr/share/zoneinfo/zone.tab' contains the list of valid names.",
669 type => 'string',
670 },
671 },
672 },
673 returns => { type => "null" },
674 code => sub {
675 my ($param) = @_;
676
677 PVE::INotify::write_file('timezone', $param->{timezone});
678
679 return undef;
680 }});
681
682 __PACKAGE__->register_method({
683 name => 'status',
684 path => 'status',
685 method => 'GET',
686 description => "Read server status. This is used by the cluster manager to test the node health.",
687 proxyto => 'node',
688 permissions => { check => [ 'admin', 'qmanager', 'audit' ] },
689 protected => 1,
690 parameters => {
691 additionalProperties => 0,
692 properties => {
693 node => get_standard_option('pve-node'),
694 },
695 },
696 returns => {
697 type => "object",
698 additionalProperties => 1,
699 properties => {
700 time => {
701 description => "Seconds since 1970-01-01 00:00:00 UTC.",
702 type => 'integer',
703 minimum => 1297163644,
704 },
705 uptime => {
706 description => "The uptime of the system in seconds.",
707 type => 'integer',
708 minimum => 0,
709 },
710 insync => {
711 description => "Database is synced with other nodes.",
712 type => 'boolean',
713 },
714 },
715 },
716 code => sub {
717 my ($param) = @_;
718
719 my $restenv = PMG::RESTEnvironment->get();
720 my $cinfo = $restenv->{cinfo};
721
722 my $ctime = time();
723
724 my $res = { time => $ctime, insync => 1 };
725
726 my $si = PMG::DBTools::cluster_sync_status($cinfo);
727 foreach my $cid (keys %$si) {
728 my $lastsync = $si->{$cid};
729 my $sdiff = $ctime - $lastsync;
730 $sdiff = 0 if $sdiff < 0;
731 $res->{insync} = 0 if $sdiff > (60*3);
732 }
733
734 my ($uptime, $idle) = PVE::ProcFSTools::read_proc_uptime();
735 $res->{uptime} = $uptime;
736
737 my ($avg1, $avg5, $avg15) = PVE::ProcFSTools::read_loadavg();
738 $res->{loadavg} = [ $avg1, $avg5, $avg15];
739
740 my ($sysname, $nodename, $release, $version, $machine) = POSIX::uname();
741
742 $res->{kversion} = "$sysname $release $version";
743
744 $res->{cpuinfo} = PVE::ProcFSTools::read_cpuinfo();
745
746 my $stat = PVE::ProcFSTools::read_proc_stat();
747 $res->{cpu} = $stat->{cpu};
748 $res->{wait} = $stat->{wait};
749
750 my $meminfo = PVE::ProcFSTools::read_meminfo();
751 $res->{memory} = {
752 free => $meminfo->{memfree},
753 total => $meminfo->{memtotal},
754 used => $meminfo->{memused},
755 };
756
757 $res->{swap} = {
758 free => $meminfo->{swapfree},
759 total => $meminfo->{swaptotal},
760 used => $meminfo->{swapused},
761 };
762
763 $res->{pmgversion} = PMG::pmgcfg::package() . "/" .
764 PMG::pmgcfg::version_text();
765
766 my $dinfo = df('/', 1); # output is bytes
767
768 $res->{rootfs} = {
769 total => $dinfo->{blocks},
770 avail => $dinfo->{bavail},
771 used => $dinfo->{used},
772 free => $dinfo->{blocks} - $dinfo->{used},
773 };
774
775 if (my $subinfo = eval { PMG::API2::Subscription::read_etc_subscription() } ) {
776 if (my $level = $subinfo->{level}) {
777 $res->{level} = $level;
778 }
779 }
780
781 return $res;
782 }});
783
784 __PACKAGE__->register_method({
785 name => 'node_cmd',
786 path => 'status',
787 method => 'POST',
788 permissions => { check => [ 'admin' ] },
789 protected => 1,
790 description => "Reboot or shutdown a node.",
791 proxyto => 'node',
792 parameters => {
793 additionalProperties => 0,
794 properties => {
795 node => get_standard_option('pve-node'),
796 command => {
797 description => "Specify the command.",
798 type => 'string',
799 enum => [qw(reboot shutdown)],
800 },
801 },
802 },
803 returns => { type => "null" },
804 code => sub {
805 my ($param) = @_;
806
807 if ($param->{command} eq 'reboot') {
808 system ("(sleep 2;/sbin/reboot)&");
809 } elsif ($param->{command} eq 'shutdown') {
810 system ("(sleep 2;/sbin/poweroff)&");
811 }
812
813 return undef;
814 }});
815
816 package PMG::API2::Nodes;
817
818 use strict;
819 use warnings;
820
821 use PVE::RESTHandler;
822 use PVE::JSONSchema qw(get_standard_option);
823
824 use PMG::RESTEnvironment;
825
826 use base qw(PVE::RESTHandler);
827
828 __PACKAGE__->register_method ({
829 subclass => "PMG::API2::NodeInfo",
830 path => '{node}',
831 });
832
833 __PACKAGE__->register_method ({
834 name => 'index',
835 path => '',
836 method => 'GET',
837 permissions => { user => 'all' },
838 description => "Cluster node index.",
839 parameters => {
840 additionalProperties => 0,
841 properties => {},
842 },
843 returns => {
844 type => 'array',
845 items => {
846 type => "object",
847 properties => {},
848 },
849 links => [ { rel => 'child', href => "{node}" } ],
850 },
851 code => sub {
852 my ($param) = @_;
853
854 my $nodename = PVE::INotify::nodename();
855
856 my $res = [ { node => $nodename } ];
857
858 my $done = {};
859
860 $done->{$nodename} = 1;
861
862 my $restenv = PMG::RESTEnvironment->get();
863 my $cinfo = $restenv->{cinfo};
864
865 foreach my $ni (values %{$cinfo->{ids}}) {
866 push @$res, { node => $ni->{name} } if !$done->{$ni->{name}};
867 $done->{$ni->{name}} = 1;
868 }
869
870 return $res;
871 }});
872
873
874 1;