]> git.proxmox.com Git - pmg-api.git/blame - bin/pmgsh
pmgperf: new cli tool
[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};
0fb55414
DM
242 my $node;
243 if ($pn eq 'master') {
244 $node = PMG::Cluster::get_master_node();
245 } else {
246 $node = $uri_param->{$pn};
247 die "proxy parameter '$pn' does not exists" if !$node;
248 }
1d18e90b
DM
249
250 if ($node ne 'localhost' && ($node ne PVE::INotify::nodename())) {
251 die "proxy loop detected - aborting\n" if $disable_proxy;
252 my $remip = PMG::Cluster::remote_node_ip($node);
253 return ($node, $remip);
254 }
255 }
256
257 return undef;
258}
259
260sub proxy_handler {
261 my ($node, $remip, $dir, $cmd, $args) = @_;
262
263 my $remcmd = ['ssh', '-o', 'BatchMode=yes', "root\@$remip",
264 'pmgsh', '--noproxy', $cmd, $dir, @$args];
265
266 system(@$remcmd) == 0 || die "proxy handler failed\n";
267}
268
269sub call_method {
270 my ($dir, $cmd, $args, $nooutput) = @_;
271
272 my $method = map_cmd($cmd);
273
274 my $uri_param = {};
275 my ($handler, $info) = PMG::API2->find_handler($method, $dir, $uri_param);
276 if (!$handler || !$info) {
277 die "no '$cmd' handler for '$dir'\n";
278 }
279
280 my ($node, $remip) = check_proxyto($info, $uri_param);
281 return proxy_handler($node, $remip, $dir, $cmd, $args) if $node;
282
283 my $data = $handler->cli_handler("$cmd $dir", $info->{name}, $args, [], $uri_param, $read_password);
284
285 return if $nooutput;
286
287 warn "200 OK\n"; # always print OK status if successful
288
289 if ($info && $info->{returns} && $info->{returns}->{type}) {
290 my $rtype = $info->{returns}->{type};
291
292 return if $rtype eq 'null';
293
294 if ($rtype eq 'string') {
295 print $data if $data;
296 return;
297 }
298 }
299
300 print to_json($data, {utf8 => 1, allow_nonref => 1, canonical => 1, pretty => 1 });
301
302 return;
303}
304
305sub find_resource_methods {
306 my ($path, $ihash) = @_;
307
308 for my $method (qw(GET POST PUT DELETE)) {
309 my $uri_param = {};
310 my $path_match;
311 my ($handler, $info) = PMG::API2->find_handler($method, $path, $uri_param, \$path_match);
312 if ($handler && $info && !$ihash->{$info}) {
313 $ihash->{$info} = {
314 path => $path_match,
315 handler => $handler,
316 info => $info,
317 uri_param => $uri_param,
318 };
319 }
320 }
321}
322
323sub print_help {
324 my ($path, $opts) = @_;
325
326 my $ihash = {};
327
328 find_resource_methods($path, $ihash);
329
330 if (!scalar(keys(%$ihash))) {
331 die "no such resource\n";
332 }
333
334 my $di = dir_info($path);
335 if (my $children = $di->{children}) {
336 foreach my $c (@$children) {
337 my $cp = abs_path($path, $c);
338 find_resource_methods($cp, $ihash);
339 }
340 }
341
342 foreach my $mi (sort { $a->{path} cmp $b->{path} } values %$ihash) {
343 my $method = $mi->{info}->{method};
344
345 # we skip index methods for now.
346 next if ($method eq 'GET') && PVE::JSONSchema::method_get_child_link($mi->{info});
347
348 my $path = $mi->{path};
349 $path =~ s|/+$||; # remove trailing slash
350
351 my $cmd = reverse_map_cmd($method);
352
353 print $mi->{handler}->usage_str($mi->{info}->{name}, "$cmd $path", [], $mi->{uri_param},
354 $opts->{verbose} ? 'full' : 'short', 1);
355 print "\n\n" if $opts->{verbose};
356 }
357
358};
359
360sub resource_cap {
361 my ($path) = @_;
362
363 my $res = '';
364
365 my ($handler, $info) = PMG::API2->find_handler('GET', $path);
366 if (!($handler && $info)) {
367 $res .= '--';
368 } else {
369 if (PVE::JSONSchema::method_get_child_link($info)) {
370 $res .= 'Dr';
371 } else {
372 $res .= '-r';
373 }
374 }
375
376 ($handler, $info) = PMG::API2->find_handler('PUT', $path);
377 if (!($handler && $info)) {
378 $res .= '-';
379 } else {
380 $res .= 'w';
381 }
382
383 ($handler, $info) = PMG::API2->find_handler('POST', $path);
384 if (!($handler && $info)) {
385 $res .= '-';
386 } else {
387 $res .= 'c';
388 }
389
390 ($handler, $info) = PMG::API2->find_handler('DELETE', $path);
391 if (!($handler && $info)) {
392 $res .= '-';
393 } else {
394 $res .= 'd';
395 }
396
397 return $res;
398}
399
400sub extract_children {
401 my ($lnk, $data) = @_;
402
403 my $res = [];
404
405 return $res if !($lnk && $data);
406
407 my $href = $lnk->{href};
408 if ($href =~ m/^\{(\S+)\}$/) {
409 my $prop = $1;
410
411 foreach my $elem (sort {$a->{$prop} cmp $b->{$prop}} @$data) {
412 next if !ref($elem);
413 my $value = $elem->{$prop};
414 push @$res, $value;
415 }
416 }
417
418 return $res;
419}
420
421sub dir_info {
422 my ($path) = @_;
423
424 my $res = { path => $path };
425 my $uri_param = {};
426 my ($handler, $info, $pm) = PMG::API2->find_handler('GET', $path, $uri_param);
427 if ($handler && $info) {
428 eval {
429 my $data = $handler->handle($info, $uri_param);
430 my $lnk = PVE::JSONSchema::method_get_child_link($info);
431 $res->{children} = extract_children($lnk, $data);
432 }; # ignore errors ?
433 }
434 return $res;
435}
436
437sub list_dir {
438 my ($dir, $args) = @_;
439
440 my $uri_param = {};
441 my ($handler, $info) = PMG::API2->find_handler('GET', $dir, $uri_param);
442 if (!$handler || !$info) {
443 die "no such resource\n";
444 }
445
446 if (!PVE::JSONSchema::method_get_child_link($info)) {
447 die "resource does not define child links\n";
448 }
449
450 my ($node, $remip) = check_proxyto($info, $uri_param);
451 return proxy_handler($node, $remip, $dir, 'ls', $args) if $node;
452
453
454 my $data = $handler->cli_handler("ls $dir", $info->{name}, $args, [], $uri_param, $read_password);
455 my $lnk = PVE::JSONSchema::method_get_child_link($info);
456 my $children = extract_children($lnk, $data);
457
458 foreach my $c (@$children) {
459 my $cap = resource_cap(abs_path($dir, $c));
460 print "$cap $c\n";
461 }
462}
463
464sub pmg_command {
465 my ($args, $nooutput) = @_;
466
467 $rpcenv->init_request();
468
469 my $cmd = shift @$args;
470
471 if ($cmd eq 'cd') {
472
473 my $path = shift @$args;
474
475 die "usage: cd [dir]\n" if scalar(@$args);
476
477 if (!defined($path)) {
478 $cdir = '';
479 return;
480 } else {
481 my $new_dir = abs_path($cdir, $path);
482 my ($handler, $info) = PMG::API2->find_handler('GET', $new_dir);
483 die "no such resource\n" if !$handler;
484 $cdir = $new_dir;
485 }
486
487 } elsif ($cmd eq 'help') {
488
489 my $help_usage_error = sub {
490 die "usage: help [path] [--verbose]\n";
491 };
492
493 my $opts = {};
494
495 &$help_usage_error() if !Getopt::Long::GetOptionsFromArray($args, $opts, 'verbose');
496
497 my $path;
498 if (scalar(@$args) && $args->[0] !~ m/^\-/) {
499 $path = shift @$args;
500 }
501
502 &$help_usage_error() if scalar(@$args);
503
504 print "help [path] [--verbose]\n";
505 print "cd [path]\n";
506 print "ls [path]\n\n";
507
508 print_help(abs_path($cdir, $path), $opts);
509
510 } elsif ($cmd eq 'ls') {
511 my $path;
512 if (scalar(@$args) && $args->[0] !~ m/^\-/) {
513 $path = shift @$args;
514 }
515
516 list_dir(abs_path($cdir, $path), $args);
517
518 } elsif ($cmd =~ m/^get|delete|set$/) {
519
520 my $path;
521 if (scalar(@$args) && $args->[0] !~ m/^\-/) {
522 $path = shift @$args;
523 }
524
525 call_method(abs_path($cdir, $path), $cmd, $args);
526
527 } elsif ($cmd eq 'create') {
528
529 my $path;
530 if (scalar(@$args) && $args->[0] !~ m/^\-/) {
531 $path = shift @$args;
532 }
533
534 call_method(abs_path($cdir, $path), $cmd, $args, $nooutput);
535
536 } else {
537 die "unknown command '$cmd'\n";
538 }
539
540}
541
542my $input;
543while (defined ($input = $term->readline("pmg:/$cdir> "))) {
544 chomp $input;
545
546 next if $input =~ m/^\s*$/;
547
548 if ($input =~ m/^\s*q(uit)?\s*$/) {
549 exit (0);
550 }
551
552 # add input to history if it gets not
553 # automatically added
554 if (!$term->Features->{autohistory}) {
555 $term->addhistory($input);
556 }
557
558 eval {
559 my $args = [ shellwords($input) ];
560 pmg_command($args);
561 };
562 warn $@ if $@;
563}
564
565__END__
566
567=head1 NAME
568
569pmgsh - shell interface to the Promox Mail Gateway API
570
571=head1 SYNOPSIS
572
573pmgsh [get|set|create|delete|help] [REST API path] [--verbose]
574
575=head1 DESCRIPTION
576
577pmgsh provides a shell-like interface to the Proxmox Mail Gateway API, in
578two different modes:
579
580=over
581
582=item interactive
583
584when called without parameters, pmgsh starts an interactive client,
585where you can navigate in the different parts of the API
586
587=item command line
588
589when started with parameters pmgsh will send a command to the
590corresponding REST url, and will return the JSON formatted output
591
592=back
593
594=head1 EXAMPLES
595
596get the list of nodes in my cluster
597
598pmgsh get /nodes
599