]> git.proxmox.com Git - pmg-api.git/blob - src/bin/pmgsh
f9816cd7e177ada7dcaf0a73305dd2e00207e134
[pmg-api.git] / src / bin / pmgsh
1 #!/usr/bin/perl
2
3 use strict;
4 use warnings;
5 use Term::ReadLine;
6 use File::Basename;
7 use Getopt::Long;
8 use HTTP::Status qw(:constants :is status_message);
9 use Text::ParseWords;
10
11 use PVE::JSONSchema;
12 use PVE::SafeSyslog;
13 use PVE::INotify;
14 use PVE::CLIHandler;
15
16 use PMG::RESTEnvironment;
17
18 use PMG::Ticket;
19 use PMG::Cluster;
20 use PMG::API2;
21
22 use JSON;
23
24 my $basedir = '/api2/json';
25
26 my $cdir = '';
27
28 sub 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
37 my $disable_proxy = 0;
38 my $opt_nooutput = 0;
39
40 my $cmd = shift;
41
42 my $optmatch;
43 do {
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
59 if ($cmd) {
60 if ($cmd eq 'verifyapi') {
61 PVE::RESTHandler::validate_method_schemas();
62 exit 0;
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');
66 pmg_command([ $cmd, @ARGV], $opt_nooutput);
67 exit(0);
68 } else {
69 print_usage ("unknown command '$cmd'");
70 exit (-1);
71 }
72 }
73
74 if (scalar (@ARGV) != 0) {
75 print_usage();
76 exit(-1);
77 }
78
79 # only set up once actually required allows calling verifyapi in restriced clean sbuild env
80 PMG::RESTEnvironment->setup_default_cli_env();
81 my $rpcenv = PMG::RESTEnvironment->get();
82 initlog($ENV{PVE_LOG_ID} || 'pmgsh');
83
84 print "entering PMG shell - type 'help' for help\n";
85
86 my $term = new Term::ReadLine('pmgsh');
87 my $attribs = $term->Attribs;
88
89 sub 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
156 sub 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
183 my $param_mapping = sub {
184 my ($name) = @_;
185
186 return [ PVE::CLIHandler::get_standard_mapping('pve-password') ];
187 };
188
189 sub reverse_map_cmd {
190 my $method = shift;
191
192 my $mmap = {
193 GET => 'get',
194 PUT => 'set',
195 POST => 'create',
196 DELETE => 'delete',
197 };
198
199 my $cmd = $mmap->{$method};
200
201 die "got strange value for method ('$method') - internal error" if !$cmd;
202
203 return $cmd;
204 }
205
206 sub map_cmd {
207 my $cmd = shift;
208
209 my $mmap = {
210 create => 'POST',
211 set => 'PUT',
212 get => 'GET',
213 ls => 'GET',
214 delete => 'DELETE',
215 };
216
217 my $method = $mmap->{$cmd};
218
219 die "unable to map method" if !$method;
220
221 return $method;
222 }
223
224 sub check_proxyto {
225 my ($info, $uri_param) = @_;
226
227 if ($info->{proxyto}) {
228 my $pn = $info->{proxyto};
229 my $node;
230 if ($pn eq 'master') {
231 $node = PMG::Cluster::get_master_node();
232 } else {
233 $node = $uri_param->{$pn};
234 die "proxy parameter '$pn' does not exists" if !$node;
235 }
236
237 if ($node ne 'localhost' && ($node ne PVE::INotify::nodename())) {
238 die "proxy loop detected - aborting\n" if $disable_proxy;
239 my $remip = PMG::Cluster::remote_node_ip($node);
240 return ($node, $remip);
241 }
242 }
243
244 return undef;
245 }
246
247 sub proxy_handler {
248 my ($node, $remip, $dir, $cmd, $args) = @_;
249
250 my $remcmd = ['ssh', '-o', 'BatchMode=yes', "root\@$remip",
251 'pmgsh', '--noproxy', $cmd, $dir, @$args];
252
253 system(@$remcmd) == 0 || die "proxy handler failed\n";
254 }
255
256 sub call_method {
257 my ($dir, $cmd, $args, $nooutput) = @_;
258
259 my $method = map_cmd($cmd);
260
261 my $uri_param = {};
262 my ($handler, $info) = PMG::API2->find_handler($method, $dir, $uri_param);
263 if (!$handler || !$info) {
264 die "no '$cmd' handler for '$dir'\n";
265 }
266
267 my ($node, $remip) = check_proxyto($info, $uri_param);
268 return proxy_handler($node, $remip, $dir, $cmd, $args) if $node;
269
270 my $data = $handler->cli_handler("$cmd $dir", $info->{name}, $args, [], $uri_param, $param_mapping);
271
272 return if $nooutput;
273
274 warn "200 OK\n"; # always print OK status if successful
275
276 if ($info && $info->{returns} && $info->{returns}->{type}) {
277 my $rtype = $info->{returns}->{type};
278
279 return if $rtype eq 'null';
280
281 if ($rtype eq 'string') {
282 print $data if $data;
283 return;
284 }
285 }
286
287 print to_json($data, {utf8 => 1, allow_nonref => 1, canonical => 1, pretty => 1 });
288
289 return;
290 }
291
292 sub find_resource_methods {
293 my ($path, $ihash) = @_;
294
295 for my $method (qw(GET POST PUT DELETE)) {
296 my $uri_param = {};
297 my $path_match;
298 my ($handler, $info) = PMG::API2->find_handler($method, $path, $uri_param, \$path_match);
299 if ($handler && $info && !$ihash->{$info}) {
300 $ihash->{$info} = {
301 path => $path_match,
302 handler => $handler,
303 info => $info,
304 uri_param => $uri_param,
305 };
306 }
307 }
308 }
309
310 sub print_help {
311 my ($path, $opts) = @_;
312
313 my $ihash = {};
314
315 find_resource_methods($path, $ihash);
316
317 if (!scalar(keys(%$ihash))) {
318 die "no such resource\n";
319 }
320
321 my $di = dir_info($path);
322 if (my $children = $di->{children}) {
323 foreach my $c (@$children) {
324 my $cp = abs_path($path, $c);
325 find_resource_methods($cp, $ihash);
326 }
327 }
328
329 foreach my $mi (sort { $a->{path} cmp $b->{path} } values %$ihash) {
330 my $method = $mi->{info}->{method};
331
332 # we skip index methods for now.
333 next if ($method eq 'GET') && PVE::JSONSchema::method_get_child_link($mi->{info});
334
335 my $path = $mi->{path};
336 $path =~ s|/+$||; # remove trailing slash
337
338 my $cmd = reverse_map_cmd($method);
339
340 print $mi->{handler}->usage_str($mi->{info}->{name}, "$cmd $path", [], $mi->{uri_param},
341 $opts->{verbose} ? 'full' : 'short');
342 print "\n\n" if $opts->{verbose};
343 }
344
345 };
346
347 sub resource_cap {
348 my ($path) = @_;
349
350 my $res = '';
351
352 my ($handler, $info) = PMG::API2->find_handler('GET', $path);
353 if (!($handler && $info)) {
354 $res .= '--';
355 } else {
356 if (PVE::JSONSchema::method_get_child_link($info)) {
357 $res .= 'Dr';
358 } else {
359 $res .= '-r';
360 }
361 }
362
363 ($handler, $info) = PMG::API2->find_handler('PUT', $path);
364 if (!($handler && $info)) {
365 $res .= '-';
366 } else {
367 $res .= 'w';
368 }
369
370 ($handler, $info) = PMG::API2->find_handler('POST', $path);
371 if (!($handler && $info)) {
372 $res .= '-';
373 } else {
374 $res .= 'c';
375 }
376
377 ($handler, $info) = PMG::API2->find_handler('DELETE', $path);
378 if (!($handler && $info)) {
379 $res .= '-';
380 } else {
381 $res .= 'd';
382 }
383
384 return $res;
385 }
386
387 sub extract_children {
388 my ($lnk, $data) = @_;
389
390 my $res = [];
391
392 return $res if !($lnk && $data);
393
394 my $href = $lnk->{href};
395 if ($href =~ m/^\{(\S+)\}$/) {
396 my $prop = $1;
397
398 foreach my $elem (sort {$a->{$prop} cmp $b->{$prop}} @$data) {
399 next if !ref($elem);
400 my $value = $elem->{$prop};
401 push @$res, $value;
402 }
403 }
404
405 return $res;
406 }
407
408 sub dir_info {
409 my ($path) = @_;
410
411 my $res = { path => $path };
412 my $uri_param = {};
413 my ($handler, $info, $pm) = PMG::API2->find_handler('GET', $path, $uri_param);
414 if ($handler && $info) {
415 eval {
416 my $data = $handler->handle($info, $uri_param);
417 my $lnk = PVE::JSONSchema::method_get_child_link($info);
418 $res->{children} = extract_children($lnk, $data);
419 }; # ignore errors ?
420 }
421 return $res;
422 }
423
424 sub list_dir {
425 my ($dir, $args) = @_;
426
427 my $uri_param = {};
428 my ($handler, $info) = PMG::API2->find_handler('GET', $dir, $uri_param);
429 if (!$handler || !$info) {
430 die "no such resource\n";
431 }
432
433 if (!PVE::JSONSchema::method_get_child_link($info)) {
434 die "resource does not define child links\n";
435 }
436
437 my ($node, $remip) = check_proxyto($info, $uri_param);
438 return proxy_handler($node, $remip, $dir, 'ls', $args) if $node;
439
440
441 my $data = $handler->cli_handler("ls $dir", $info->{name}, $args, [], $uri_param, $param_mapping);
442 my $lnk = PVE::JSONSchema::method_get_child_link($info);
443 my $children = extract_children($lnk, $data);
444
445 foreach my $c (@$children) {
446 my $cap = resource_cap(abs_path($dir, $c));
447 print "$cap $c\n";
448 }
449 }
450
451 sub pmg_command {
452 my ($args, $nooutput) = @_;
453
454 $rpcenv->init_request();
455
456 my $ticket = PMG::Ticket::assemble_ticket('root@pam');
457
458 $rpcenv->set_ticket($ticket);
459 $rpcenv->set_user('root@pam');
460 $rpcenv->set_role('root');
461
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
535 my $input;
536 while (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
562 pmgsh - shell interface to the Promox Mail Gateway API
563
564 =head1 SYNOPSIS
565
566 pmgsh [get|set|create|delete|help] [REST API path] [--verbose]
567
568 =head1 DESCRIPTION
569
570 pmgsh provides a shell-like interface to the Proxmox Mail Gateway API, in
571 two different modes:
572
573 =over
574
575 =item interactive
576
577 when called without parameters, pmgsh starts an interactive client,
578 where you can navigate in the different parts of the API
579
580 =item command line
581
582 when started with parameters pmgsh will send a command to the
583 corresponding REST url, and will return the JSON formatted output
584
585 =back
586
587 =head1 EXAMPLES
588
589 get the list of nodes in my cluster
590
591 pmgsh get /nodes
592