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