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