]> git.proxmox.com Git - pmg-api.git/blame - bin/pmgsh
pmg-daily: add check for available updates
[pmg-api.git] / bin / pmgsh
CommitLineData
1d18e90b
DM
1#!/usr/bin/perl
2
3use strict;
4use warnings;
5use Term::ReadLine;
6use File::Basename;
7use Getopt::Long;
8use HTTP::Status qw(:constants :is status_message);
9use Text::ParseWords;
10
11use PVE::JSONSchema;
12use PVE::SafeSyslog;
13use PVE::INotify;
807a5c2e
DM
14
15use PMG::RESTEnvironment;
1d18e90b 16
ba11e2d3 17use PMG::Ticket;
1d18e90b
DM
18use PMG::Cluster;
19use PMG::API2;
ba11e2d3 20
1d18e90b
DM
21use JSON;
22
2030e673 23PMG::RESTEnvironment->setup_default_cli_env();
1d18e90b 24
2030e673 25my $rpcenv = PMG::RESTEnvironment->get();
1d18e90b 26
ba11e2d3 27my $ticket = PMG::Ticket::assemble_ticket('root@pam');
1d18e90b
DM
28
29my $logid = $ENV{PVE_LOG_ID} || 'pmgsh';
30initlog($logid);
31
32my $basedir = '/api2/json';
33
34my $cdir = '';
35
36sub print_usage {
37 my $msg = shift;
38
39 print STDERR "ERROR: $msg\n" if $msg;
40 print STDERR "USAGE: pmgsh [verifyapi]\n";
41 print STDERR " pmgsh CMD [OPTIONS]\n";
42
43}
44
45my $disable_proxy = 0;
46my $opt_nooutput = 0;
47
48my $cmd = shift;
49
50my $optmatch;
51do {
52 $optmatch = 0;
53 if ($cmd) {
54 if ($cmd eq '--noproxy') {
55 $cmd = shift;
56 $disable_proxy = 1;
57 $optmatch = 1;
58 } elsif ($cmd eq '--nooutput') {
59 # we use this when starting task in CLI (suppress printing upid)
60 $cmd = shift;
61 $opt_nooutput = 1;
62 $optmatch = 1;
63 }
64 }
65} while ($optmatch);
66
67if ($cmd) {
68 if ($cmd eq 'verifyapi') {
69 PVE::RESTHandler::validate_method_schemas();
70 exit 0;
71 } elsif ($cmd eq 'ls' || $cmd eq 'get' || $cmd eq 'create' ||
72 $cmd eq 'set' || $cmd eq 'delete' ||$cmd eq 'help' ) {
73 pmg_command([ $cmd, @ARGV], $opt_nooutput);
74 exit(0);
75 } else {
76 print_usage ("unknown command '$cmd'");
77 exit (-1);
78 }
79}
80
81if (scalar (@ARGV) != 0) {
82 print_usage();
83 exit(-1);
84}
85
86print "entering PMG shell - type 'help' for help\n";
87
88my $term = new Term::ReadLine('pmgsh');
89my $attribs = $term->Attribs;
90
91sub complete_path {
92 my($text) = @_;
93
94 my ($dir, undef, $rest) = $text =~ m|^(.*/)?(([^/]*))?$|;
95 my $path = abs_path($cdir, $dir);
96
97 my @res = ();
98
99 my $di = dir_info($path);
100 if (my $children = $di->{children}) {
101 foreach my $c (@$children) {
102 if ($c =~ /^\Q$rest/) {
103 my $new = $dir ? "$dir$c" : $c;
104 push @res, $new;
105 }
106 }
107 }
108
109 if (scalar(@res) == 0) {
110 return undef;
111 } elsif (scalar(@res) == 1) {
112 return ($res[0], $res[0], "$res[0]/");
113 }
114
115 # lcd : lowest common denominator
116 my $lcd = '';
117 my $tmp = $res[0];
118 for (my $i = 1; $i <= length($tmp); $i++) {
119 my $found = 1;
120 foreach my $p (@res) {
121 if (substr($tmp, 0, $i) ne substr($p, 0, $i)) {
122 $found = 0;
123 last;
124 }
125 }
126 if ($found) {
127 $lcd = substr($tmp, 0, $i);
128 } else {
129 last;
130 }
131 }
132
133 return ($lcd, @res);
134};
135
136# just to avoid an endless loop (called by attempted_completion_function)
137$attribs->{completion_entry_function} = sub {
138 my($text, $state) = @_;
139 return undef;
140};
141
142$attribs->{attempted_completion_function} = sub {
143 my ($text, $line, $start) = @_;
144
145 my $prefix = substr($line, 0, $start);
146 if ($prefix =~ /^\s*$/) { # first word (command completeion)
147 $attribs->{completion_word} = [qw(help ls cd get set create delete quit)];
148 return $term->completion_matches($text, $attribs->{list_completion_function});
149 }
150
151 if ($prefix =~ /^\s*\S+\s+$/) { # second word (path completion)
152 return complete_path($text);
153 }
154
155 return ();
156};
157
158sub abs_path {
159 my ($current, $path) = @_;
160
161 my $ret = $current;
162
163 return $current if !defined($path);
164
165 $ret = '' if $path =~ m|^\/|;
166
167 foreach my $d (split (/\/+/ , $path)) {
168 if ($d eq '.') {
169 next;
170 } elsif ($d eq '..') {
171 $ret = dirname($ret);
172 $ret = '' if $ret eq '.';
173 } else {
174 $ret = "$ret/$d";
175 }
176 }
177
178 $ret =~ s|\/+|\/|g;
179 $ret =~ s|^\/||;
180 $ret =~ s|\/$||;
181
182 return $ret;
183}
184
185my $read_password = sub {
186 my $attribs = $term->Attribs;
187 my $old = $attribs->{redisplay_function};
188 $attribs->{redisplay_function} = $attribs->{shadow_redisplay};
189 my $input = $term->readline('password: ');
190 my $conf = $term->readline('Retype new password: ');
191 $attribs->{redisplay_function} = $old;
192
193 # remove password from history
194 if ($term->Features->{autohistory}) {
195 my $historyPosition = $term->where_history();
196 $term->remove_history($historyPosition);
197 $term->remove_history($historyPosition - 1);
198 }
199
200 die "Passwords do not match.\n" if ($input ne $conf);
201 return $input;
202};
203
204sub reverse_map_cmd {
205 my $method = shift;
206
207 my $mmap = {
208 GET => 'get',
209 PUT => 'set',
210 POST => 'create',
211 DELETE => 'delete',
212 };
213
214 my $cmd = $mmap->{$method};
215
216 die "got strange value for method ('$method') - internal error" if !$cmd;
217
218 return $cmd;
219}
220
221sub map_cmd {
222 my $cmd = shift;
223
224 my $mmap = {
225 create => 'POST',
226 set => 'PUT',
227 get => 'GET',
228 ls => 'GET',
229 delete => 'DELETE',
230 };
231
232 my $method = $mmap->{$cmd};
233
234 die "unable to map method" if !$method;
235
236 return $method;
237}
238
239sub check_proxyto {
240 my ($info, $uri_param) = @_;
241
242 if ($info->{proxyto}) {
243 my $pn = $info->{proxyto};
0fb55414
DM
244 my $node;
245 if ($pn eq 'master') {
246 $node = PMG::Cluster::get_master_node();
247 } else {
248 $node = $uri_param->{$pn};
249 die "proxy parameter '$pn' does not exists" if !$node;
250 }
1d18e90b
DM
251
252 if ($node ne 'localhost' && ($node ne PVE::INotify::nodename())) {
253 die "proxy loop detected - aborting\n" if $disable_proxy;
254 my $remip = PMG::Cluster::remote_node_ip($node);
255 return ($node, $remip);
256 }
257 }
258
259 return undef;
260}
261
262sub proxy_handler {
263 my ($node, $remip, $dir, $cmd, $args) = @_;
264
265 my $remcmd = ['ssh', '-o', 'BatchMode=yes', "root\@$remip",
266 'pmgsh', '--noproxy', $cmd, $dir, @$args];
267
268 system(@$remcmd) == 0 || die "proxy handler failed\n";
269}
270
271sub call_method {
272 my ($dir, $cmd, $args, $nooutput) = @_;
273
274 my $method = map_cmd($cmd);
275
276 my $uri_param = {};
277 my ($handler, $info) = PMG::API2->find_handler($method, $dir, $uri_param);
278 if (!$handler || !$info) {
279 die "no '$cmd' handler for '$dir'\n";
280 }
281
282 my ($node, $remip) = check_proxyto($info, $uri_param);
283 return proxy_handler($node, $remip, $dir, $cmd, $args) if $node;
284
285 my $data = $handler->cli_handler("$cmd $dir", $info->{name}, $args, [], $uri_param, $read_password);
286
287 return if $nooutput;
288
289 warn "200 OK\n"; # always print OK status if successful
290
291 if ($info && $info->{returns} && $info->{returns}->{type}) {
292 my $rtype = $info->{returns}->{type};
293
294 return if $rtype eq 'null';
295
296 if ($rtype eq 'string') {
297 print $data if $data;
298 return;
299 }
300 }
301
302 print to_json($data, {utf8 => 1, allow_nonref => 1, canonical => 1, pretty => 1 });
303
304 return;
305}
306
307sub find_resource_methods {
308 my ($path, $ihash) = @_;
309
310 for my $method (qw(GET POST PUT DELETE)) {
311 my $uri_param = {};
312 my $path_match;
313 my ($handler, $info) = PMG::API2->find_handler($method, $path, $uri_param, \$path_match);
314 if ($handler && $info && !$ihash->{$info}) {
315 $ihash->{$info} = {
316 path => $path_match,
317 handler => $handler,
318 info => $info,
319 uri_param => $uri_param,
320 };
321 }
322 }
323}
324
325sub print_help {
326 my ($path, $opts) = @_;
327
328 my $ihash = {};
329
330 find_resource_methods($path, $ihash);
331
332 if (!scalar(keys(%$ihash))) {
333 die "no such resource\n";
334 }
335
336 my $di = dir_info($path);
337 if (my $children = $di->{children}) {
338 foreach my $c (@$children) {
339 my $cp = abs_path($path, $c);
340 find_resource_methods($cp, $ihash);
341 }
342 }
343
344 foreach my $mi (sort { $a->{path} cmp $b->{path} } values %$ihash) {
345 my $method = $mi->{info}->{method};
346
347 # we skip index methods for now.
348 next if ($method eq 'GET') && PVE::JSONSchema::method_get_child_link($mi->{info});
349
350 my $path = $mi->{path};
351 $path =~ s|/+$||; # remove trailing slash
352
353 my $cmd = reverse_map_cmd($method);
354
355 print $mi->{handler}->usage_str($mi->{info}->{name}, "$cmd $path", [], $mi->{uri_param},
356 $opts->{verbose} ? 'full' : 'short', 1);
357 print "\n\n" if $opts->{verbose};
358 }
359
360};
361
362sub resource_cap {
363 my ($path) = @_;
364
365 my $res = '';
366
367 my ($handler, $info) = PMG::API2->find_handler('GET', $path);
368 if (!($handler && $info)) {
369 $res .= '--';
370 } else {
371 if (PVE::JSONSchema::method_get_child_link($info)) {
372 $res .= 'Dr';
373 } else {
374 $res .= '-r';
375 }
376 }
377
378 ($handler, $info) = PMG::API2->find_handler('PUT', $path);
379 if (!($handler && $info)) {
380 $res .= '-';
381 } else {
382 $res .= 'w';
383 }
384
385 ($handler, $info) = PMG::API2->find_handler('POST', $path);
386 if (!($handler && $info)) {
387 $res .= '-';
388 } else {
389 $res .= 'c';
390 }
391
392 ($handler, $info) = PMG::API2->find_handler('DELETE', $path);
393 if (!($handler && $info)) {
394 $res .= '-';
395 } else {
396 $res .= 'd';
397 }
398
399 return $res;
400}
401
402sub extract_children {
403 my ($lnk, $data) = @_;
404
405 my $res = [];
406
407 return $res if !($lnk && $data);
408
409 my $href = $lnk->{href};
410 if ($href =~ m/^\{(\S+)\}$/) {
411 my $prop = $1;
412
413 foreach my $elem (sort {$a->{$prop} cmp $b->{$prop}} @$data) {
414 next if !ref($elem);
415 my $value = $elem->{$prop};
416 push @$res, $value;
417 }
418 }
419
420 return $res;
421}
422
423sub dir_info {
424 my ($path) = @_;
425
426 my $res = { path => $path };
427 my $uri_param = {};
428 my ($handler, $info, $pm) = PMG::API2->find_handler('GET', $path, $uri_param);
429 if ($handler && $info) {
430 eval {
431 my $data = $handler->handle($info, $uri_param);
432 my $lnk = PVE::JSONSchema::method_get_child_link($info);
433 $res->{children} = extract_children($lnk, $data);
434 }; # ignore errors ?
435 }
436 return $res;
437}
438
439sub list_dir {
440 my ($dir, $args) = @_;
441
442 my $uri_param = {};
443 my ($handler, $info) = PMG::API2->find_handler('GET', $dir, $uri_param);
444 if (!$handler || !$info) {
445 die "no such resource\n";
446 }
447
448 if (!PVE::JSONSchema::method_get_child_link($info)) {
449 die "resource does not define child links\n";
450 }
451
452 my ($node, $remip) = check_proxyto($info, $uri_param);
453 return proxy_handler($node, $remip, $dir, 'ls', $args) if $node;
454
455
456 my $data = $handler->cli_handler("ls $dir", $info->{name}, $args, [], $uri_param, $read_password);
457 my $lnk = PVE::JSONSchema::method_get_child_link($info);
458 my $children = extract_children($lnk, $data);
459
460 foreach my $c (@$children) {
461 my $cap = resource_cap(abs_path($dir, $c));
462 print "$cap $c\n";
463 }
464}
465
466sub pmg_command {
467 my ($args, $nooutput) = @_;
468
469 $rpcenv->init_request();
470
ba11e2d3 471 $rpcenv->set_ticket($ticket);
2030e673
DM
472 $rpcenv->set_user('root@pam');
473 $rpcenv->set_role('root');
ba11e2d3 474
1d18e90b
DM
475 my $cmd = shift @$args;
476
477 if ($cmd eq 'cd') {
478
479 my $path = shift @$args;
480
481 die "usage: cd [dir]\n" if scalar(@$args);
482
483 if (!defined($path)) {
484 $cdir = '';
485 return;
486 } else {
487 my $new_dir = abs_path($cdir, $path);
488 my ($handler, $info) = PMG::API2->find_handler('GET', $new_dir);
489 die "no such resource\n" if !$handler;
490 $cdir = $new_dir;
491 }
492
493 } elsif ($cmd eq 'help') {
494
495 my $help_usage_error = sub {
496 die "usage: help [path] [--verbose]\n";
497 };
498
499 my $opts = {};
500
501 &$help_usage_error() if !Getopt::Long::GetOptionsFromArray($args, $opts, 'verbose');
502
503 my $path;
504 if (scalar(@$args) && $args->[0] !~ m/^\-/) {
505 $path = shift @$args;
506 }
507
508 &$help_usage_error() if scalar(@$args);
509
510 print "help [path] [--verbose]\n";
511 print "cd [path]\n";
512 print "ls [path]\n\n";
513
514 print_help(abs_path($cdir, $path), $opts);
515
516 } elsif ($cmd eq 'ls') {
517 my $path;
518 if (scalar(@$args) && $args->[0] !~ m/^\-/) {
519 $path = shift @$args;
520 }
521
522 list_dir(abs_path($cdir, $path), $args);
523
524 } elsif ($cmd =~ m/^get|delete|set$/) {
525
526 my $path;
527 if (scalar(@$args) && $args->[0] !~ m/^\-/) {
528 $path = shift @$args;
529 }
530
531 call_method(abs_path($cdir, $path), $cmd, $args);
532
533 } elsif ($cmd eq 'create') {
534
535 my $path;
536 if (scalar(@$args) && $args->[0] !~ m/^\-/) {
537 $path = shift @$args;
538 }
539
540 call_method(abs_path($cdir, $path), $cmd, $args, $nooutput);
541
542 } else {
543 die "unknown command '$cmd'\n";
544 }
545
546}
547
548my $input;
549while (defined ($input = $term->readline("pmg:/$cdir> "))) {
550 chomp $input;
551
552 next if $input =~ m/^\s*$/;
553
554 if ($input =~ m/^\s*q(uit)?\s*$/) {
555 exit (0);
556 }
557
558 # add input to history if it gets not
559 # automatically added
560 if (!$term->Features->{autohistory}) {
561 $term->addhistory($input);
562 }
563
564 eval {
565 my $args = [ shellwords($input) ];
566 pmg_command($args);
567 };
568 warn $@ if $@;
569}
570
571__END__
572
573=head1 NAME
574
575pmgsh - shell interface to the Promox Mail Gateway API
576
577=head1 SYNOPSIS
578
579pmgsh [get|set|create|delete|help] [REST API path] [--verbose]
580
581=head1 DESCRIPTION
582
583pmgsh provides a shell-like interface to the Proxmox Mail Gateway API, in
584two different modes:
585
586=over
587
588=item interactive
589
590when called without parameters, pmgsh starts an interactive client,
591where you can navigate in the different parts of the API
592
593=item command line
594
595when started with parameters pmgsh will send a command to the
596corresponding REST url, and will return the JSON formatted output
597
598=back
599
600=head1 EXAMPLES
601
602get the list of nodes in my cluster
603
604pmgsh get /nodes
605