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