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