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