]> git.proxmox.com Git - pmg-api.git/blob - PMG/API2/Nodes.pm
PMG/API2/Nodes.pm: add node status API
[pmg-api.git] / 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
8 use PVE::INotify;
9 use PVE::RESTHandler;
10 use PVE::JSONSchema qw(get_standard_option);
11 use PMG::RESTEnvironment;
12 use PVE::SafeSyslog;
13 use PVE::ProcFSTools;
14
15 use PMG::pmgcfg;
16 use PMG::Ticket;
17 use PMG::API2::Tasks;
18 use PMG::API2::Services;
19 use PMG::API2::Network;
20 use PMG::API2::ClamAV;
21 use PMG::API2::Postfix;
22
23 use base qw(PVE::RESTHandler);
24
25 __PACKAGE__->register_method ({
26 subclass => "PMG::API2::Postfix",
27 path => 'postfix',
28 });
29
30 __PACKAGE__->register_method ({
31 subclass => "PMG::API2::ClamAV",
32 path => 'clamav',
33 });
34
35 __PACKAGE__->register_method ({
36 subclass => "PMG::API2::Network",
37 path => 'network',
38 });
39
40 __PACKAGE__->register_method ({
41 subclass => "PMG::API2::Tasks",
42 path => 'tasks',
43 });
44
45 __PACKAGE__->register_method ({
46 subclass => "PMG::API2::Services",
47 path => 'services',
48 });
49
50 __PACKAGE__->register_method ({
51 name => 'index',
52 path => '',
53 method => 'GET',
54 permissions => { user => 'all' },
55 description => "Node index.",
56 parameters => {
57 additionalProperties => 0,
58 properties => {
59 node => get_standard_option('pve-node'),
60 },
61 },
62 returns => {
63 type => 'array',
64 items => {
65 type => "object",
66 properties => {},
67 },
68 links => [ { rel => 'child', href => "{name}" } ],
69 },
70 code => sub {
71 my ($param) = @_;
72
73 my $result = [
74 { name => 'clamav' },
75 { name => 'postfix' },
76 { name => 'services' },
77 { name => 'syslog' },
78 { name => 'tasks' },
79 { name => 'time' },
80 { name => 'status' },
81 { name => 'vncshell' },
82 { name => 'rrddata' },
83 ];
84
85 return $result;
86 }});
87
88 __PACKAGE__->register_method({
89 name => 'rrddata',
90 path => 'rrddata',
91 method => 'GET',
92 protected => 1, # fixme: can we avoid that?
93 proxyto => 'node',
94 description => "Read node RRD statistics",
95 parameters => {
96 additionalProperties => 0,
97 properties => {
98 node => get_standard_option('pve-node'),
99 timeframe => {
100 description => "Specify the time frame you are interested in.",
101 type => 'string',
102 enum => [ 'hour', 'day', 'week', 'month', 'year' ],
103 },
104 cf => {
105 description => "The RRD consolidation function",
106 type => 'string',
107 enum => [ 'AVERAGE', 'MAX' ],
108 optional => 1,
109 },
110 },
111 },
112 returns => {
113 type => "array",
114 items => {
115 type => "object",
116 properties => {},
117 },
118 },
119 code => sub {
120 my ($param) = @_;
121
122 return PMG::Utils::create_rrd_data(
123 "pmg-node-v1.rrd", $param->{timeframe}, $param->{cf});
124 }});
125
126
127 __PACKAGE__->register_method({
128 name => 'syslog',
129 path => 'syslog',
130 method => 'GET',
131 description => "Read system log",
132 proxyto => 'node',
133 protected => 1,
134 parameters => {
135 additionalProperties => 0,
136 properties => {
137 node => get_standard_option('pve-node'),
138 start => {
139 type => 'integer',
140 minimum => 0,
141 optional => 1,
142 },
143 limit => {
144 type => 'integer',
145 minimum => 0,
146 optional => 1,
147 },
148 since => {
149 type => 'string',
150 pattern => '^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}(:\d{2})?)?$',
151 description => "Display all log since this date-time string.",
152 optional => 1,
153 },
154 'until' => {
155 type => 'string',
156 pattern => '^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}(:\d{2})?)?$',
157 description => "Display all log until this date-time string.",
158 optional => 1,
159 },
160 service => {
161 description => "Service ID",
162 type => 'string',
163 maxLength => 128,
164 optional => 1,
165 },
166 },
167 },
168 returns => {
169 type => 'array',
170 items => {
171 type => "object",
172 properties => {
173 n => {
174 description=> "Line number",
175 type=> 'integer',
176 },
177 t => {
178 description=> "Line text",
179 type => 'string',
180 }
181 }
182 }
183 },
184 code => sub {
185 my ($param) = @_;
186
187 my $restenv = PMG::RESTEnvironment->get();
188
189 my $service = $param->{service};
190 if ($service && $service eq 'postfix') {
191 $service = 'postfix@-';
192 }
193
194 my ($count, $lines) = PVE::Tools::dump_journal(
195 $param->{start}, $param->{limit},
196 $param->{since}, $param->{'until'}, $service);
197
198 $restenv->set_result_attrib('total', $count);
199
200 return $lines;
201 }});
202
203 __PACKAGE__->register_method ({
204 name => 'vncshell',
205 path => 'vncshell',
206 method => 'POST',
207 protected => 1,
208 description => "Creates a VNC Shell proxy.",
209 parameters => {
210 additionalProperties => 0,
211 properties => {
212 node => get_standard_option('pve-node'),
213 websocket => {
214 optional => 1,
215 type => 'boolean',
216 description => "use websocket instead of standard vnc.",
217 default => 1,
218 },
219 },
220 },
221 returns => {
222 additionalProperties => 0,
223 properties => {
224 user => { type => 'string' },
225 ticket => { type => 'string' },
226 port => { type => 'integer' },
227 upid => { type => 'string' },
228 },
229 },
230 code => sub {
231 my ($param) = @_;
232
233 my $node = $param->{node};
234
235 # we only implement the websocket based VNC here
236 my $websocket = $param->{websocket} // 1;
237 die "standard VNC not implemented" if !$websocket;
238
239 my $authpath = "/nodes/$node";
240
241 my $restenv = PMG::RESTEnvironment->get();
242 my $user = $restenv->get_user();
243
244 my $ticket = PMG::Ticket::assemble_vnc_ticket($user, $authpath);
245
246 my $family = PVE::Tools::get_host_address_family($node);
247 my $port = PVE::Tools::next_vnc_port($family);
248
249 my $shcmd;
250
251 if ($user eq 'root@pam') {
252 $shcmd = [ '/bin/login', '-f', 'root' ];
253 } else {
254 $shcmd = [ '/bin/login' ];
255 }
256
257 my $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
258 '-timeout', 10, '-notls', '-listen', 'localhost',
259 '-c', @$shcmd];
260
261 my $realcmd = sub {
262 my $upid = shift;
263
264 syslog ('info', "starting vnc proxy $upid\n");
265
266 my $cmdstr = join (' ', @$cmd);
267 syslog ('info', "launch command: $cmdstr");
268
269 eval {
270 foreach my $k (keys %ENV) {
271 next if $k eq 'PATH' || $k eq 'TERM' || $k eq 'USER' || $k eq 'HOME';
272 delete $ENV{$k};
273 }
274 $ENV{PWD} = '/';
275
276 $ENV{PVE_VNC_TICKET} = $ticket; # pass ticket to vncterm
277
278 PVE::Tools::run_command($cmd, errmsg => "vncterm failed");
279 };
280 if (my $err = $@) {
281 syslog('err', $err);
282 }
283
284 return;
285 };
286
287 my $upid = $restenv->fork_worker('vncshell', "", $user, $realcmd);
288
289 PVE::Tools::wait_for_vnc_port($port);
290
291 return {
292 user => $user,
293 ticket => $ticket,
294 port => $port,
295 upid => $upid,
296 };
297 }});
298
299 __PACKAGE__->register_method({
300 name => 'vncwebsocket',
301 path => 'vncwebsocket',
302 method => 'GET',
303 description => "Opens a weksocket for VNC traffic.",
304 parameters => {
305 additionalProperties => 0,
306 properties => {
307 node => get_standard_option('pve-node'),
308 vncticket => {
309 description => "Ticket from previous call to vncproxy.",
310 type => 'string',
311 maxLength => 512,
312 },
313 port => {
314 description => "Port number returned by previous vncproxy call.",
315 type => 'integer',
316 minimum => 5900,
317 maximum => 5999,
318 },
319 },
320 },
321 returns => {
322 type => "object",
323 properties => {
324 port => { type => 'string' },
325 },
326 },
327 code => sub {
328 my ($param) = @_;
329
330 my $authpath = "/nodes/$param->{node}";
331
332 my $restenv = PMG::RESTEnvironment->get();
333 my $user = $restenv->get_user();
334
335 PMG::Ticket::verify_vnc_ticket($param->{vncticket}, $user, $authpath);
336
337 my $port = $param->{port};
338
339 return { port => $port };
340 }});
341
342 __PACKAGE__->register_method({
343 name => 'dns',
344 path => 'dns',
345 method => 'GET',
346 description => "Read DNS settings.",
347 proxyto => 'node',
348 parameters => {
349 additionalProperties => 0,
350 properties => {
351 node => get_standard_option('pve-node'),
352 },
353 },
354 returns => {
355 type => "object",
356 additionalProperties => 0,
357 properties => {
358 search => {
359 description => "Search domain for host-name lookup.",
360 type => 'string',
361 optional => 1,
362 },
363 dns1 => {
364 description => 'First name server IP address.',
365 type => 'string',
366 optional => 1,
367 },
368 dns2 => {
369 description => 'Second name server IP address.',
370 type => 'string',
371 optional => 1,
372 },
373 dns3 => {
374 description => 'Third name server IP address.',
375 type => 'string',
376 optional => 1,
377 },
378 },
379 },
380 code => sub {
381 my ($param) = @_;
382
383 my $res = PVE::INotify::read_file('resolvconf');
384
385 return $res;
386 }});
387
388 __PACKAGE__->register_method({
389 name => 'update_dns',
390 path => 'dns',
391 method => 'PUT',
392 description => "Write DNS settings.",
393 proxyto => 'node',
394 protected => 1,
395 parameters => {
396 additionalProperties => 0,
397 properties => {
398 node => get_standard_option('pve-node'),
399 search => {
400 description => "Search domain for host-name lookup.",
401 type => 'string',
402 },
403 dns1 => {
404 description => 'First name server IP address.',
405 type => 'string', format => 'ip',
406 optional => 1,
407 },
408 dns2 => {
409 description => 'Second name server IP address.',
410 type => 'string', format => 'ip',
411 optional => 1,
412 },
413 dns3 => {
414 description => 'Third name server IP address.',
415 type => 'string', format => 'ip',
416 optional => 1,
417 },
418 },
419 },
420 returns => { type => "null" },
421 code => sub {
422 my ($param) = @_;
423
424 PVE::INotify::update_file('resolvconf', $param);
425
426 return undef;
427 }});
428
429
430 __PACKAGE__->register_method({
431 name => 'time',
432 path => 'time',
433 method => 'GET',
434 description => "Read server time and time zone settings.",
435 proxyto => 'node',
436 parameters => {
437 additionalProperties => 0,
438 properties => {
439 node => get_standard_option('pve-node'),
440 },
441 },
442 returns => {
443 type => "object",
444 additionalProperties => 0,
445 properties => {
446 timezone => {
447 description => "Time zone",
448 type => 'string',
449 },
450 time => {
451 description => "Seconds since 1970-01-01 00:00:00 UTC.",
452 type => 'integer',
453 minimum => 1297163644,
454 },
455 localtime => {
456 description => "Seconds since 1970-01-01 00:00:00 (local time)",
457 type => 'integer',
458 minimum => 1297163644,
459 },
460 },
461 },
462 code => sub {
463 my ($param) = @_;
464
465 my $ctime = time();
466 my $ltime = timegm_nocheck(localtime($ctime));
467 my $res = {
468 timezone => PVE::INotify::read_file('timezone'),
469 time => time(),
470 localtime => $ltime,
471 };
472
473 return $res;
474 }});
475
476 __PACKAGE__->register_method({
477 name => 'set_timezone',
478 path => 'time',
479 method => 'PUT',
480 description => "Set time zone.",
481 proxyto => 'node',
482 protected => 1,
483 parameters => {
484 additionalProperties => 0,
485 properties => {
486 node => get_standard_option('pve-node'),
487 timezone => {
488 description => "Time zone. The file '/usr/share/zoneinfo/zone.tab' contains the list of valid names.",
489 type => 'string',
490 },
491 },
492 },
493 returns => { type => "null" },
494 code => sub {
495 my ($param) = @_;
496
497 PVE::INotify::write_file('timezone', $param->{timezone});
498
499 return undef;
500 }});
501
502 __PACKAGE__->register_method({
503 name => 'status',
504 path => 'status',
505 method => 'GET',
506 description => "Read server status. This is used by the cluster manager to test the node health.",
507 proxyto => 'node',
508 protected => 1,
509 parameters => {
510 additionalProperties => 0,
511 properties => {
512 node => get_standard_option('pve-node'),
513 },
514 },
515 returns => {
516 type => "object",
517 additionalProperties => 1,
518 properties => {
519 time => {
520 description => "Seconds since 1970-01-01 00:00:00 UTC.",
521 type => 'integer',
522 minimum => 1297163644,
523 },
524 uptime => {
525 description => "The uptime of the system in seconds.",
526 type => 'integer',
527 minimum => 0,
528 },
529 insync => {
530 description => "Database is synced with other nodes.",
531 type => 'boolean',
532 },
533 },
534 },
535 code => sub {
536 my ($param) = @_;
537
538 my $restenv = PMG::RESTEnvironment->get();
539 my $cinfo = $restenv->{cinfo};
540
541 my $ctime = time();
542
543 my $res = { time => $ctime, insync => 1 };
544
545 my $si = PMG::DBTools::cluster_sync_status($cinfo);
546 foreach my $cid (keys %$si) {
547 my $lastsync = $si->{$cid};
548 my $sdiff = $ctime - $lastsync;
549 $sdiff = 0 if $sdiff < 0;
550 $res->{insync} = 0 if $sdiff > (60*3);
551 }
552
553 my ($uptime, $idle) = PVE::ProcFSTools::read_proc_uptime();
554 $res->{uptime} = $uptime;
555
556 my ($avg1, $avg5, $avg15) = PVE::ProcFSTools::read_loadavg();
557 $res->{loadavg} = [ $avg1, $avg5, $avg15];
558
559 my ($sysname, $nodename, $release, $version, $machine) = POSIX::uname();
560
561 $res->{kversion} = "$sysname $release $version";
562
563 $res->{cpuinfo} = PVE::ProcFSTools::read_cpuinfo();
564
565 my $stat = PVE::ProcFSTools::read_proc_stat();
566 $res->{cpu} = $stat->{cpu};
567 $res->{wait} = $stat->{wait};
568
569 my $meminfo = PVE::ProcFSTools::read_meminfo();
570 $res->{memory} = {
571 free => $meminfo->{memfree},
572 total => $meminfo->{memtotal},
573 used => $meminfo->{memused},
574 };
575
576 $res->{swap} = {
577 free => $meminfo->{swapfree},
578 total => $meminfo->{swaptotal},
579 used => $meminfo->{swapused},
580 };
581
582 $res->{pmgversion} = PMG::pmgcfg::package() . "/" .
583 PMG::pmgcfg::version_text();
584
585 my $dinfo = df('/', 1); # output is bytes
586
587 $res->{rootfs} = {
588 total => $dinfo->{blocks},
589 avail => $dinfo->{bavail},
590 used => $dinfo->{used},
591 free => $dinfo->{bavail} - $dinfo->{used},
592 };
593
594 return $res;
595 }});
596
597
598 package PMG::API2::Nodes;
599
600 use strict;
601 use warnings;
602
603 use PVE::RESTHandler;
604 use PVE::JSONSchema qw(get_standard_option);
605
606 use PMG::RESTEnvironment;
607
608 use base qw(PVE::RESTHandler);
609
610 __PACKAGE__->register_method ({
611 subclass => "PMG::API2::NodeInfo",
612 path => '{node}',
613 });
614
615 __PACKAGE__->register_method ({
616 name => 'index',
617 path => '',
618 method => 'GET',
619 permissions => { user => 'all' },
620 description => "Cluster node index.",
621 parameters => {
622 additionalProperties => 0,
623 properties => {},
624 },
625 returns => {
626 type => 'array',
627 items => {
628 type => "object",
629 properties => {},
630 },
631 links => [ { rel => 'child', href => "{node}" } ],
632 },
633 code => sub {
634 my ($param) = @_;
635
636 my $nodename = PVE::INotify::nodename();
637
638 my $res = [ { node => $nodename } ];
639
640 my $done = {};
641
642 $done->{$nodename} = 1;
643
644 my $restenv = PMG::RESTEnvironment->get();
645 my $cinfo = $restenv->{cinfo};
646
647 foreach my $ni (values %{$cinfo->{ids}}) {
648 push @$res, { node => $ni->{name} } if !$done->{$ni->{name}};
649 $done->{$ni->{name}} = 1;
650 }
651
652 return $res;
653 }});
654
655
656 1;