]>
Commit | Line | Data |
---|---|---|
1d18e90b DM |
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; | |
8319c8d9 | 14 | use PVE::CLIHandler; |
807a5c2e DM |
15 | |
16 | use PMG::RESTEnvironment; | |
1d18e90b | 17 | |
ba11e2d3 | 18 | use PMG::Ticket; |
1d18e90b DM |
19 | use PMG::Cluster; |
20 | use PMG::API2; | |
ba11e2d3 | 21 | |
1d18e90b DM |
22 | use JSON; |
23 | ||
1d18e90b DM |
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; | |
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 | ||
74 | if (scalar (@ARGV) != 0) { | |
75 | print_usage(); | |
76 | exit(-1); | |
77 | } | |
78 | ||
15832835 TL |
79 | # only set up once actually required allows calling verifyapi in restriced clean sbuild env |
80 | PMG::RESTEnvironment->setup_default_cli_env(); | |
15832835 TL |
81 | initlog($ENV{PVE_LOG_ID} || 'pmgsh'); |
82 | ||
1d18e90b DM |
83 | print "entering PMG shell - type 'help' for help\n"; |
84 | ||
85 | my $term = new Term::ReadLine('pmgsh'); | |
86 | my $attribs = $term->Attribs; | |
87 | ||
88 | sub 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); | |
143 | if ($prefix =~ /^\s*$/) { # first word (command completeion) | |
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 | ||
155 | sub 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 |
182 | my $param_mapping = sub { |
183 | my ($name) = @_; | |
1d18e90b | 184 | |
8319c8d9 | 185 | return [ PVE::CLIHandler::get_standard_mapping('pve-password') ]; |
1d18e90b DM |
186 | }; |
187 | ||
188 | sub 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 | ||
205 | sub 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 | ||
223 | sub 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 | ||
246 | sub 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 | ||
255 | sub 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 | ||
291 | sub 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 | ||
309 | sub 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 | ||
346 | sub 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 | ||
386 | sub 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 | ||
407 | sub 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 | ||
423 | sub 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 | ||
450 | sub 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 | ||
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 |