8 use HTTP
::Status
qw(:constants :is status_message);
16 use PMG
::RESTEnvironment
;
24 my $basedir = '/api2/json';
31 print STDERR
"ERROR: $msg\n" if $msg;
32 print STDERR
"USAGE: pmgsh [verifyapi]\n";
33 print STDERR
" pmgsh CMD [OPTIONS]\n";
37 my $disable_proxy = 0;
46 if ($cmd eq '--noproxy') {
50 } elsif ($cmd eq '--nooutput') {
51 # we use this when starting task in CLI (suppress printing upid)
60 if ($cmd eq 'verifyapi') {
61 PVE
::RESTHandler
::validate_method_schemas
();
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);
69 print_usage
("unknown command '$cmd'");
74 if (scalar (@ARGV) != 0) {
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');
84 print "entering PMG shell - type 'help' for help\n";
86 my $term = new Term
::ReadLine
('pmgsh');
87 my $attribs = $term->Attribs;
92 my ($dir, undef, $rest) = $text =~ m
|^(.*/)?(([^/]*))?
$|;
93 my $path = abs_path
($cdir, $dir);
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;
107 if (scalar(@res) == 0) {
109 } elsif (scalar(@res) == 1) {
110 return ($res[0], $res[0], "$res[0]/");
113 # lcd : lowest common denominator
116 for (my $i = 1; $i <= length($tmp); $i++) {
118 foreach my $p (@res) {
119 if (substr($tmp, 0, $i) ne substr($p, 0, $i)) {
125 $lcd = substr($tmp, 0, $i);
134 # just to avoid an endless loop (called by attempted_completion_function)
135 $attribs->{completion_entry_function
} = sub {
136 my($text, $state) = @_;
140 $attribs->{attempted_completion_function
} = sub {
141 my ($text, $line, $start) = @_;
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
});
149 if ($prefix =~ /^\s*\S+\s+$/) { # second word (path completion)
150 return complete_path
($text);
157 my ($current, $path) = @_;
161 return $current if !defined($path);
163 $ret = '' if $path =~ m
|^\
/|;
165 foreach my $d (split (/\/+/ , $path)) {
168 } elsif ($d eq '..') {
169 $ret = dirname
($ret);
170 $ret = '' if $ret eq '.';
183 my $param_mapping = sub {
186 return [ PVE
::CLIHandler
::get_standard_mapping
('pve-password') ];
189 sub reverse_map_cmd
{
199 my $cmd = $mmap->{$method};
201 die "got strange value for method ('$method') - internal error" if !$cmd;
217 my $method = $mmap->{$cmd};
219 die "unable to map method" if !$method;
225 my ($info, $uri_param) = @_;
227 if ($info->{proxyto
}) {
228 my $pn = $info->{proxyto
};
230 if ($pn eq 'master') {
231 $node = PMG
::Cluster
::get_master_node
();
233 $node = $uri_param->{$pn};
234 die "proxy parameter '$pn' does not exists" if !$node;
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);
248 my ($node, $remip, $dir, $cmd, $args) = @_;
250 my $remcmd = ['ssh', '-o', 'BatchMode=yes', "root\@$remip",
251 'pmgsh', '--noproxy', $cmd, $dir, @$args];
253 system(@$remcmd) == 0 || die "proxy handler failed\n";
257 my ($dir, $cmd, $args, $nooutput) = @_;
259 my $method = map_cmd
($cmd);
262 my ($handler, $info) = PMG
::API2-
>find_handler($method, $dir, $uri_param);
263 if (!$handler || !$info) {
264 die "no '$cmd' handler for '$dir'\n";
267 my ($node, $remip) = check_proxyto
($info, $uri_param);
268 return proxy_handler
($node, $remip, $dir, $cmd, $args) if $node;
270 my $data = $handler->cli_handler("$cmd $dir", $info->{name
}, $args, [], $uri_param, $param_mapping);
274 warn "200 OK\n"; # always print OK status if successful
276 if ($info && $info->{returns
} && $info->{returns
}->{type
}) {
277 my $rtype = $info->{returns
}->{type
};
279 return if $rtype eq 'null';
281 if ($rtype eq 'string') {
282 print $data if $data;
287 print to_json
($data, {utf8
=> 1, allow_nonref
=> 1, canonical
=> 1, pretty
=> 1 });
292 sub find_resource_methods
{
293 my ($path, $ihash) = @_;
295 for my $method (qw(GET POST PUT DELETE)) {
298 my ($handler, $info) = PMG
::API2-
>find_handler($method, $path, $uri_param, \
$path_match);
299 if ($handler && $info && !$ihash->{$info}) {
304 uri_param
=> $uri_param,
311 my ($path, $opts) = @_;
315 find_resource_methods
($path, $ihash);
317 if (!scalar(keys(%$ihash))) {
318 die "no such resource\n";
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);
329 foreach my $mi (sort { $a->{path
} cmp $b->{path
} } values %$ihash) {
330 my $method = $mi->{info
}->{method};
332 # we skip index methods for now.
333 next if ($method eq 'GET') && PVE
::JSONSchema
::method_get_child_link
($mi->{info
});
335 my $path = $mi->{path
};
336 $path =~ s
|/+$||; # remove trailing slash
338 my $cmd = reverse_map_cmd
($method);
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
};
352 my ($handler, $info) = PMG
::API2-
>find_handler('GET', $path);
353 if (!($handler && $info)) {
356 if (PVE
::JSONSchema
::method_get_child_link
($info)) {
363 ($handler, $info) = PMG
::API2-
>find_handler('PUT', $path);
364 if (!($handler && $info)) {
370 ($handler, $info) = PMG
::API2-
>find_handler('POST', $path);
371 if (!($handler && $info)) {
377 ($handler, $info) = PMG
::API2-
>find_handler('DELETE', $path);
378 if (!($handler && $info)) {
387 sub extract_children
{
388 my ($lnk, $data) = @_;
392 return $res if !($lnk && $data);
394 my $href = $lnk->{href
};
395 if ($href =~ m/^\{(\S+)\}$/) {
398 foreach my $elem (sort {$a->{$prop} cmp $b->{$prop}} @$data) {
400 my $value = $elem->{$prop};
411 my $res = { path
=> $path };
413 my ($handler, $info, $pm) = PMG
::API2-
>find_handler('GET', $path, $uri_param);
414 if ($handler && $info) {
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);
425 my ($dir, $args) = @_;
428 my ($handler, $info) = PMG
::API2-
>find_handler('GET', $dir, $uri_param);
429 if (!$handler || !$info) {
430 die "no such resource\n";
433 if (!PVE
::JSONSchema
::method_get_child_link
($info)) {
434 die "resource does not define child links\n";
437 my ($node, $remip) = check_proxyto
($info, $uri_param);
438 return proxy_handler
($node, $remip, $dir, 'ls', $args) if $node;
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);
445 foreach my $c (@$children) {
446 my $cap = resource_cap
(abs_path
($dir, $c));
452 my ($args, $nooutput) = @_;
454 $rpcenv->init_request();
456 my $ticket = PMG
::Ticket
::assemble_ticket
('root@pam');
458 $rpcenv->set_ticket($ticket);
459 $rpcenv->set_user('root@pam');
460 $rpcenv->set_role('root');
462 my $cmd = shift @$args;
466 my $path = shift @$args;
468 die "usage: cd [dir]\n" if scalar(@$args);
470 if (!defined($path)) {
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;
480 } elsif ($cmd eq 'help') {
482 my $help_usage_error = sub {
483 die "usage: help [path] [--verbose]\n";
488 &$help_usage_error() if !Getopt
::Long
::GetOptionsFromArray
($args, $opts, 'verbose');
491 if (scalar(@$args) && $args->[0] !~ m/^\-/) {
492 $path = shift @$args;
495 &$help_usage_error() if scalar(@$args);
497 print "help [path] [--verbose]\n";
499 print "ls [path]\n\n";
501 print_help
(abs_path
($cdir, $path), $opts);
503 } elsif ($cmd eq 'ls') {
505 if (scalar(@$args) && $args->[0] !~ m/^\-/) {
506 $path = shift @$args;
509 list_dir
(abs_path
($cdir, $path), $args);
511 } elsif ($cmd =~ m/^get|delete|set$/) {
514 if (scalar(@$args) && $args->[0] !~ m/^\-/) {
515 $path = shift @$args;
518 call_method
(abs_path
($cdir, $path), $cmd, $args);
520 } elsif ($cmd eq 'create') {
523 if (scalar(@$args) && $args->[0] !~ m/^\-/) {
524 $path = shift @$args;
527 call_method
(abs_path
($cdir, $path), $cmd, $args, $nooutput);
530 die "unknown command '$cmd'\n";
536 while (defined ($input = $term->readline("pmg:/$cdir> "))) {
539 next if $input =~ m/^\s*$/;
541 if ($input =~ m/^\s*q(uit)?\s*$/) {
545 # add input to history if it gets not
546 # automatically added
547 if (!$term->Features->{autohistory
}) {
548 $term->addhistory($input);
552 my $args = [ shellwords
($input) ];
562 pmgsh - shell interface to the Promox Mail Gateway API
566 pmgsh [get|set|create|delete|help] [REST API path] [--verbose]
570 pmgsh provides a shell-like interface to the Proxmox Mail Gateway API, in
577 when called without parameters, pmgsh starts an interactive client,
578 where you can navigate in the different parts of the API
582 when started with parameters pmgsh will send a command to the
583 corresponding REST url, and will return the JSON formatted output
589 get the list of nodes in my cluster